HarmonyOS开发:场景联动——自动化场景

HarmonyOS开发:场景联动------自动化场景

📌 核心要点:场景联动是智能家居的灵魂,条件-动作模型驱动自动化,定时触发和事件触发两种模式,场景冲突检测避免"空调开制热又开制冷"的尴尬。

背景与动机

你每天回家,手动开灯、开空调、关窗帘------三个动作,点六次。早上出门,关灯、关空调、关窗帘------又是六次。一天两次,一个月60次,一年720次。

你不烦吗?

场景联动就是解决这个问题的。一个触发条件,自动执行一组动作。 回家模式:开客厅灯 + 空调制冷26° + 窗帘半开。离家模式:全屋关灯 + 空调关闭 + 窗帘全关。一键搞定,甚至不用按------检测到你到家了,自动触发。

但场景联动做起来没那么简单。温度高于28°自动开空调,但你已经手动关了空调------场景要不要覆盖你的手动操作?两个场景同时触发,一个开灯一个关灯------听谁的?定时场景在App被杀后台后还能不能触发?

这些问题不解决,场景联动就不是"智能",而是"智障"。

核心原理

条件-动作模型

场景联动的核心是条件-动作模型(Condition-Action):当满足条件时,执行对应的动作。
渲染错误: Mermaid 渲染失败: Parse error on line 32: ...ss A,B,C,D,E,trigger class F,H,condi -----------------------^ Expecting 'SPACE', 'AMP', 'COLON', 'DOWN', 'DEFAULT', 'NUM', 'COMMA', 'NODE_STRING', 'BRKT', 'MINUS', 'MULT', 'UNICODE_TEXT', got 'NEWLINE'

触发类型

触发类型 说明 示例
定时触发 到达指定时间触发 每天7:00开灯、工作日8:30关空调
事件触发 设备状态变化触发 温度>28°开空调、门打开开灯
手动触发 用户主动点击执行 回家模式、离家模式、睡眠模式
地理围栏 进入/离开指定区域触发 到家自动开灯、离家自动关灯

条件组合

条件可以组合,支持AND和OR:

  • AND:所有条件都满足才触发。如"温度>28° AND 湿度>70%" → 开空调制冷+除湿
  • OR:任一条件满足就触发。如"温度>28° OR 湿度>70%" → 开空调

实际项目中AND用得更多,因为场景通常需要多个条件同时满足才有意义。

动作列表

一个场景包含多个动作,按顺序执行:

json 复制代码
{
  "sceneId": "scene-home",
  "name": "回家模式",
  "actions": [
    { "deviceId": "light-001", "property": "power", "value": true },
    { "deviceId": "ac-001", "property": "power", "value": true },
    { "deviceId": "ac-001", "property": "temperature", "value": 26 },
    { "deviceId": "ac-001", "property": "mode", "value": "cool" },
    { "deviceId": "curtain-001", "property": "position", "value": 50 }
  ]
}

动作之间可以有延时:开灯后等2秒再开空调,避免同时发送太多指令导致设备处理不过来。

场景冲突

两个场景同时操作同一设备的同一属性,就是冲突。比如"高温开制冷"和"低温开制热"同时满足(虽然不太可能),或者"回家模式"开灯和"睡眠模式"关灯同时触发。

冲突检测规则:新场景优先级 > 旧场景优先级。 如果优先级相同,按触发时间排序,后触发的覆盖先触发的。

代码实战

基础用法:场景规则定义与执行引擎

定义场景规则的数据结构,实现场景执行引擎。

typescript 复制代码
// services/SceneEngine.ets
// 场景联动引擎 - 规则定义、条件判断、动作执行

// 条件操作符
type ConditionOp = 'eq' | 'ne' | 'gt' | 'gte' | 'lt' | 'lte'

// 触发条件
interface SceneCondition {
  type: 'timer' | 'event' | 'geofence'
  // 定时条件
  cronExpression?: string      // cron表达式,如'0 7 * * *'每天7:00
  // 事件条件
  deviceId?: string            // 监听的设备ID
  property?: string            // 监听的属性
  op?: ConditionOp             // 比较操作符
  value?: Object               // 比较值
  // 条件组合
  logic?: 'and' | 'or'        // 多条件的逻辑关系
  subConditions?: SceneCondition[]  // 子条件列表
}

// 场景动作
interface SceneAction {
  deviceId: string             // 目标设备ID
  property: string             // 控制属性
  value: Object                // 目标值
  delay?: number               // 延时(毫秒)
}

// 场景定义
interface SceneRule {
  sceneId: string
  name: string
  icon?: Resource
  enabled: boolean             // 是否启用
  priority: number             // 优先级,数值越大优先级越高
  conditions: SceneCondition[] // 触发条件(AND关系)
  actions: SceneAction[]       // 执行动作
  createdAt: number
  updatedAt: number
}

// 场景执行结果
interface SceneResult {
  sceneId: string
  success: boolean
  executedActions: number      // 成功执行的动作数
  failedActions: number        // 失败的动作数
  conflicts: string[]          // 冲突的场景ID列表
}

class SceneEngine {
  // 已注册的场景规则
  private rules: Map<string, SceneRule> = new Map()
  // 正在执行的场景
  private executingScenes: Set<string> = new Set()
  // 动作执行回调
  private actionExecutor?: (deviceId: string, property: string, value: Object) => Promise<boolean>
  // 定时器映射
  private timerMap: Map<string, number> = new Map()

  // 注册动作执行器(由外部注入设备控制能力)
  setActionExecutor(executor: (deviceId: string, property: string, value: Object) => Promise<boolean>): void {
    this.actionExecutor = executor
  }

  // 添加场景规则
  addRule(rule: SceneRule): void {
    this.rules.set(rule.sceneId, rule)
    if (rule.enabled) {
      this.activateRule(rule)
    }
  }

  // 移除场景规则
  removeRule(sceneId: string): void {
    const rule = this.rules.get(sceneId)
    if (rule) {
      this.deactivateRule(rule)
      this.rules.delete(sceneId)
    }
  }

  // 启用规则
  enableRule(sceneId: string): void {
    const rule = this.rules.get(sceneId)
    if (rule) {
      rule.enabled = true
      this.activateRule(rule)
    }
  }

  // 禁用规则
  disableRule(sceneId: string): void {
    const rule = this.rules.get(sceneId)
    if (rule) {
      rule.enabled = false
      this.deactivateRule(rule)
    }
  }

  // 激活规则(注册定时器等)
  private activateRule(rule: SceneRule): void {
    // 对定时条件,注册定时器
    rule.conditions.forEach((condition, index) => {
      if (condition.type === 'timer' && condition.cronExpression) {
        const timerId = this.registerCronTimer(rule.sceneId, condition.cronExpression)
        this.timerMap.set(`${rule.sceneId}_timer_${index}`, timerId)
      }
    })
  }

  // 停用规则
  private deactivateRule(rule: SceneRule): void {
    // 清除定时器
    this.timerMap.forEach((timerId, key) => {
      if (key.startsWith(rule.sceneId)) {
        clearInterval(timerId)
        this.timerMap.delete(key)
      }
    })
  }

  // 注册cron定时器(简化版,实际项目用cron库)
  private registerCronTimer(sceneId: string, cronExpression: string): number {
    // 解析cron表达式,计算下次执行时间
    // 这里简化为每分钟检查一次
    return setInterval(() => {
      if (this.matchCron(cronExpression)) {
        this.executeScene(sceneId, 'timer')
      }
    }, 60000)
  }

  // 匹配cron表达式(简化版)
  private matchCron(cron: string): boolean {
    const now = new Date()
    const parts = cron.split(' ')
    // cron格式:分 时 日 月 周
    if (parts.length !== 5) return false

    const matchMinute = parts[0] === '*' || parseInt(parts[0]) === now.getMinutes()
    const matchHour = parts[1] === '*' || parseInt(parts[1]) === now.getHours()
    const matchDay = parts[2] === '*' || parseInt(parts[2]) === now.getDate()
    const matchMonth = parts[3] === '*' || parseInt(parts[3]) === (now.getMonth() + 1)
    const matchWeek = parts[4] === '*' || parseInt(parts[4]) === now.getDay()

    return matchMinute && matchHour && matchDay && matchMonth && matchWeek
  }

  // 事件触发检查
  onDeviceStateChange(deviceId: string, property: string, value: Object): void {
    this.rules.forEach((rule) => {
      if (!rule.enabled) return

      // 检查事件条件
      const eventConditions = rule.conditions.filter(c => c.type === 'event')
      if (eventConditions.length === 0) return

      const matched = eventConditions.some(condition => {
        if (condition.deviceId !== deviceId || condition.property !== property) {
          return false
        }
        return this.evaluateCondition(value, condition.op!, condition.value)
      })

      if (matched) {
        this.executeScene(rule.sceneId, 'event')
      }
    })
  }

  // 评估条件
  private evaluateCondition(actual: Object, op: ConditionOp, expected: Object): boolean {
    const a = actual as number
    const e = expected as number

    switch (op) {
      case 'eq': return actual === expected
      case 'ne': return actual !== expected
      case 'gt': return a > e
      case 'gte': return a >= e
      case 'lt': return a < e
      case 'lte': return a <= e
      default: return false
    }
  }

  // 执行场景
  async executeScene(sceneId: string, triggerType: string): Promise<SceneResult> {
    const rule = this.rules.get(sceneId)
    if (!rule || !rule.enabled) {
      return { sceneId, success: false, executedActions: 0, failedActions: 0, conflicts: [] }
    }

    // 防止重复执行
    if (this.executingScenes.has(sceneId)) {
      return { sceneId, success: false, executedActions: 0, failedActions: 0, conflicts: [] }
    }

    this.executingScenes.add(sceneId)

    // 冲突检测
    const conflicts = this.detectConflicts(rule)

    let executedActions = 0
    let failedActions = 0

    // 按序执行动作
    for (const action of rule.actions) {
      // 检查该动作是否与冲突场景冲突
      const isConflicting = conflicts.some(conflictSceneId => {
        const conflictRule = this.rules.get(conflictSceneId)
        return conflictRule?.actions.some(a =>
          a.deviceId === action.deviceId && a.property === action.property
        )
      })

      if (isConflicting && conflicts.length > 0) {
        // 简化处理:冲突时跳过该动作
        failedActions++
        continue
      }

      // 延时执行
      if (action.delay && action.delay > 0) {
        await new Promise(resolve => setTimeout(resolve, action.delay))
      }

      // 执行动作
      if (this.actionExecutor) {
        const success = await this.actionExecutor(action.deviceId, action.property, action.value)
        if (success) {
          executedActions++
        } else {
          failedActions++
        }
      }
    }

    this.executingScenes.delete(sceneId)

    return {
      sceneId,
      success: failedActions === 0,
      executedActions,
      failedActions,
      conflicts
    }
  }

  // 冲突检测
  private detectConflicts(rule: SceneRule): string[] {
    const conflicts: string[] = []

    this.rules.forEach((otherRule, otherSceneId) => {
      if (otherSceneId === rule.sceneId || !otherRule.enabled) return
      if (!this.executingScenes.has(otherSceneId)) return

      // 检查是否有动作操作同一设备的同一属性
      const hasOverlap = rule.actions.some(action =>
        otherRule.actions.some(otherAction =>
          otherAction.deviceId === action.deviceId &&
          otherAction.property === action.property &&
          otherAction.value !== action.value  // 值不同才算冲突
        )
      )

      if (hasOverlap) {
        conflicts.push(otherSceneId)
      }
    })

    return conflicts
  }

  // 获取所有规则
  getAllRules(): SceneRule[] {
    return Array.from(this.rules.values())
  }

  // 获取规则
  getRule(sceneId: string): SceneRule | undefined {
    return this.rules.get(sceneId)
  }
}

export default new SceneEngine()
export type { SceneRule, SceneCondition, SceneAction, SceneResult }

进阶用法:场景联动页面

场景创建和编辑页面,支持条件配置、动作配置、冲突提示。

typescript 复制代码
// pages/SceneEditPage.ets
// 场景编辑页面 - 创建和编辑自动化场景

import SceneEngine, { SceneRule, SceneCondition, SceneAction } from '../services/SceneEngine'

@Entry
@Component
struct SceneEditPage {
  @State sceneName: string = ''
  @State conditions: SceneCondition[] = []
  @State actions: SceneAction[] = []
  @State isEditing: boolean = false
  @State editingSceneId: string = ''

  // 可选设备列表(简化)
  private devices = [
    { deviceId: 'light-001', name: '客厅主灯', properties: ['power', 'brightness'] },
    { deviceId: 'ac-001', name: '卧室空调', properties: ['power', 'temperature', 'mode'] },
    { deviceId: 'sensor-001', name: '温湿度传感器', properties: ['temperature', 'humidity'] },
    { deviceId: 'curtain-001', name: '客厅窗帘', properties: ['position'] }
  ]

  build() {
    Navigation() {
      Scroll() {
        Column() {
          // 场景名称
          this.NameSection()

          // 触发条件
          this.ConditionSection()

          // 执行动作
          this.ActionSection()

          // 保存按钮
          Button('保存场景')
            .width('90%')
            .height(48)
            .backgroundColor('#007DFF')
            .fontColor('#FFFFFF')
            .borderRadius(24)
            .margin({ top: 24, bottom: 24 })
            .onClick(() => this.saveScene())
        }
        .width('100%')
        .padding(16)
      }
      .width('100%')
      .height('100%')
      .backgroundColor('#F8F8F8')
    }
    .title(this.isEditing ? '编辑场景' : '创建场景')
    .titleMode(NavigationTitleMode.Mini)
  }

  // 场景名称
  @Builder NameSection() {
    Column() {
      Text('场景名称').fontSize(14).fontColor('#999999').margin({ bottom: 8 })
      TextInput({ placeholder: '如:回家模式、睡眠模式', text: this.sceneName })
        .fontSize(16)
        .onChange((value: string) => { this.sceneName = value })
    }
    .width('100%')
    .padding(16)
    .backgroundColor('#FFFFFF')
    .borderRadius(12)
  }

  // 触发条件
  @Builder ConditionSection() {
    Column() {
      Row() {
        Text('触发条件').fontSize(14).fontColor('#999999')
        Blank()
        Text('添加条件')
          .fontSize(14)
          .fontColor('#007DFF')
          .onClick(() => {
            this.conditions.push({
              type: 'event',
              deviceId: 'sensor-001',
              property: 'temperature',
              op: 'gt',
              value: 28
            })
          })
      }
      .width('100%')
      .margin({ bottom: 12 })

      // 条件列表
      ForEach(this.conditions, (condition: SceneCondition, index: number) => {
        Row() {
          if (condition.type === 'timer') {
            Text(`定时: ${condition.cronExpression || '未设置'}`)
              .fontSize(14).fontColor('#333333')
          } else {
            const device = this.devices.find(d => d.deviceId === condition.deviceId)
            Text(`当 ${device?.name || '设备'} 的 ${condition.property} ${this.getOpLabel(condition.op!)} ${condition.value}`)
              .fontSize(14).fontColor('#333333')
          }

          Blank()

          // 删除条件
          Image($r('sys.media.ohos_ic_public_remove'))
            .width(20).height(20).fillColor('#F44336')
            .onClick(() => {
              this.conditions.splice(index, 1)
            })
        }
        .width('100%')
        .padding(12)
        .backgroundColor('#F5F5F5')
        .borderRadius(8)
        .margin({ bottom: 8 })
      })

      if (this.conditions.length === 0) {
        Text('暂无触发条件,请添加')
          .fontSize(13).fontColor('#999999')
          .padding(12)
      }
    }
    .width('100%')
    .padding(16)
    .backgroundColor('#FFFFFF')
    .borderRadius(12)
    .margin({ top: 12 })
  }

  // 执行动作
  @Builder ActionSection() {
    Column() {
      Row() {
        Text('执行动作').fontSize(14).fontColor('#999999')
        Blank()
        Text('添加动作')
          .fontSize(14)
          .fontColor('#007DFF')
          .onClick(() => {
            this.actions.push({
              deviceId: 'light-001',
              property: 'power',
              value: true
            })
          })
      }
      .width('100%')
      .margin({ bottom: 12 })

      // 动作列表
      ForEach(this.actions, (action: SceneAction, index: number) => {
        Row() {
          const device = this.devices.find(d => d.deviceId === action.deviceId)
          Text(`${device?.name || '设备'} → ${action.property} = ${action.value}`)
            .fontSize(14).fontColor('#333333')

          Blank()

          // 延时设置
          Text(action.delay ? `延时${action.delay}ms` : '无延时')
            .fontSize(12).fontColor('#999999').margin({ right: 8 })

          Image($r('sys.media.ohos_ic_public_remove'))
            .width(20).height(20).fillColor('#F44336')
            .onClick(() => {
              this.actions.splice(index, 1)
            })
        }
        .width('100%')
        .padding(12)
        .backgroundColor('#F5F5F5')
        .borderRadius(8)
        .margin({ bottom: 8 })
      })

      if (this.actions.length === 0) {
        Text('暂无执行动作,请添加')
          .fontSize(13).fontColor('#999999')
          .padding(12)
      }
    }
    .width('100%')
    .padding(16)
    .backgroundColor('#FFFFFF')
    .borderRadius(12)
    .margin({ top: 12 })
  }

  // 获取操作符标签
  private getOpLabel(op: string): string {
    const labels: Record<string, string> = {
      'eq': '等于', 'ne': '不等于', 'gt': '大于',
      'gte': '大于等于', 'lt': '小于', 'lte': '小于等于'
    }
    return labels[op] || op
  }

  // 保存场景
  private saveScene(): void {
    if (!this.sceneName) {
      // 提示输入名称
      return
    }
    if (this.conditions.length === 0) {
      // 提示添加条件
      return
    }
    if (this.actions.length === 0) {
      // 提示添加动作
      return
    }

    const rule: SceneRule = {
      sceneId: this.editingSceneId || `scene_${Date.now()}`,
      name: this.sceneName,
      enabled: true,
      priority: 0,
      conditions: this.conditions,
      actions: this.actions,
      createdAt: Date.now(),
      updatedAt: Date.now()
    }

    SceneEngine.addRule(rule)
    // 返回上一页
  }
}

完整示例:场景管理主页面

场景列表展示、快捷执行、启用禁用,整合场景引擎。

typescript 复制代码
// pages/SceneListPage.ets
// 场景管理主页面

import SceneEngine, { SceneRule, SceneResult } from '../services/SceneEngine'
import DeviceControlManager from '../services/DeviceControlManager'

@Entry
@Component
struct SceneListPage {
  @State scenes: SceneRule[] = []
  @State executingSceneId: string = ''
  @State lastResult: SceneResult | null = null

  aboutToAppear() {
    // 注入动作执行器
    SceneEngine.setActionExecutor(async (deviceId, property, value) => {
      DeviceControlManager.controlDevice(deviceId, property, value)
      return true
    })

    this.loadScenes()
  }

  loadScenes() {
    // 加载预设场景
    const presets: SceneRule[] = [
      {
        sceneId: 'scene-home', name: '回家模式', enabled: true, priority: 1,
        conditions: [{ type: 'event', deviceId: 'geofence', property: 'status', op: 'eq', value: 'arrive_home' }],
        actions: [
          { deviceId: 'light-001', property: 'power', value: true },
          { deviceId: 'ac-001', property: 'power', value: true, delay: 500 },
          { deviceId: 'ac-001', property: 'temperature', value: 26, delay: 1000 },
          { deviceId: 'curtain-001', property: 'position', value: 50, delay: 1500 }
        ],
        createdAt: Date.now(), updatedAt: Date.now()
      },
      {
        sceneId: 'scene-away', name: '离家模式', enabled: true, priority: 1,
        conditions: [{ type: 'event', deviceId: 'geofence', property: 'status', op: 'eq', value: 'leave_home' }],
        actions: [
          { deviceId: 'light-001', property: 'power', value: false },
          { deviceId: 'ac-001', property: 'power', value: false, delay: 500 },
          { deviceId: 'curtain-001', property: 'position', value: 0, delay: 1000 }
        ],
        createdAt: Date.now(), updatedAt: Date.now()
      },
      {
        sceneId: 'scene-sleep', name: '睡眠模式', enabled: true, priority: 2,
        conditions: [{ type: 'timer', cronExpression: '0 23 * * *' }],
        actions: [
          { deviceId: 'light-001', property: 'power', value: false },
          { deviceId: 'ac-001', property: 'temperature', value: 27, delay: 500 },
          { deviceId: 'curtain-001', property: 'position', value: 0, delay: 1000 }
        ],
        createdAt: Date.now(), updatedAt: Date.now()
      },
      {
        sceneId: 'scene-hot', name: '高温制冷', enabled: true, priority: 3,
        conditions: [{ type: 'event', deviceId: 'sensor-001', property: 'temperature', op: 'gt', value: 28 }],
        actions: [
          { deviceId: 'ac-001', property: 'power', value: true },
          { deviceId: 'ac-001', property: 'mode', value: 'cool', delay: 500 },
          { deviceId: 'ac-001', property: 'temperature', value: 26, delay: 1000 }
        ],
        createdAt: Date.now(), updatedAt: Date.now()
      }
    ]

    presets.forEach(rule => SceneEngine.addRule(rule))
    this.scenes = SceneEngine.getAllRules()
  }

  // 手动执行场景
  async executeScene(sceneId: string) {
    this.executingSceneId = sceneId
    const result = await SceneEngine.executeScene(sceneId, 'manual')
    this.lastResult = result
    this.executingSceneId = ''

    if (result.conflicts.length > 0) {
      // 有冲突,提示用户
      console.warn(`场景冲突: ${result.conflicts.join(', ')}`)
    }
  }

  // 切换场景启用状态
  toggleScene(sceneId: string, enabled: boolean) {
    if (enabled) {
      SceneEngine.disableRule(sceneId)
    } else {
      SceneEngine.enableRule(sceneId)
    }
    this.scenes = SceneEngine.getAllRules()
  }

  build() {
    Navigation() {
      Column() {
        // 场景列表
        List({ space: 12 }) {
          ForEach(this.scenes, (scene: SceneRule) => {
            ListItem() {
              Row() {
                // 场景图标和名称
                Column() {
                  Text(scene.name)
                    .fontSize(16)
                    .fontWeight(FontWeight.Bold)
                    .fontColor('#333333')

                  // 触发条件描述
                  Text(this.getConditionDesc(scene))
                    .fontSize(12)
                    .fontColor('#999999')
                    .margin({ top: 4 })
                    .maxLines(1)
                    .textOverflow({ overflow: TextOverflow.Ellipsis })
                }
                .alignItems(HorizontalAlign.Start)
                .layoutWeight(1)

                // 启用/禁用开关
                Toggle({ type: ToggleType.Switch, isOn: scene.enabled })
                  .selectedColor('#007DFF')
                  .width(48)
                  .height(28)
                  .onChange((isOn: boolean) => {
                    this.toggleScene(scene.sceneId, scene.enabled)
                  })

                // 手动执行按钮
                Button('执行')
                  .height(32)
                  .fontSize(13)
                  .fontColor('#007DFF')
                  .backgroundColor('#F0F7FF')
                  .borderRadius(16)
                  .margin({ left: 8 })
                  .enabled(this.executingSceneId !== scene.sceneId)
                  .onClick(() => this.executeScene(scene.sceneId))
              }
              .width('100%')
              .padding(16)
              .backgroundColor('#FFFFFF')
              .borderRadius(12)
            }
          })
        }
        .width('100%')
        .layoutWeight(1)
        .padding({ left: 16, right: 16, top: 12 })

        // 执行结果提示
        if (this.lastResult) {
          Row() {
            Image(this.lastResult.success ? $r('sys.media.ohos_ic_public_ok') : $r('sys.media.ohos_ic_public_fail'))
              .width(16).height(16)
              .fillColor(this.lastResult.success ? '#4CAF50' : '#F44336')
            Text(this.lastResult.success ? '场景执行成功' : `执行失败:${this.lastResult.failedActions}个动作未完成`)
              .fontSize(13)
              .fontColor(this.lastResult.success ? '#4CAF50' : '#F44336')
              .margin({ left: 8 })
          }
          .width('100%')
          .padding(12)
          .backgroundColor(this.lastResult.success ? '#E8F5E9' : '#FFEBEE')
        }

        // 添加场景按钮
        Button('+ 创建新场景')
          .width('90%')
          .height(48)
          .backgroundColor('#007DFF')
          .fontColor('#FFFFFF')
          .borderRadius(24)
          .margin({ top: 12, bottom: 24 })
          .onClick(() => {
            // 跳转到场景编辑页面
          })
      }
      .width('100%')
      .height('100%')
      .backgroundColor('#F8F8F8')
    }
    .title('场景联动')
    .titleMode(NavigationTitleMode.Mini)
  }

  // 获取条件描述
  private getConditionDesc(scene: SceneRule): string {
    return scene.conditions.map(c => {
      if (c.type === 'timer') {
        return `定时: ${c.cronExpression}`
      } else if (c.type === 'event') {
        return `当${c.deviceId}的${c.property} ${this.getOpLabel(c.op!)} ${c.value}`
      } else {
        return '手动触发'
      }
    }).join(' 且 ')
  }

  private getOpLabel(op: string): string {
    const labels: Record<string, string> = {
      'eq': '=', 'ne': '≠', 'gt': '>', 'gte': '≥', 'lt': '<', 'lte': '≤'
    }
    return labels[op] || op
  }
}

踩坑与注意事项

1. 定时场景的后台保活

定时场景(如每天23:00执行睡眠模式)依赖App后台运行。但HarmonyOS和iOS一样,对后台App的管控很严格------App进后台一段时间后可能被杀掉,定时器就没了。

解法: 定时场景不能依赖客户端定时器,必须注册到系统的后台任务调度器。HarmonyOS用@ohos.resourceschedule.reminderAgentManager注册提醒,到时间系统会唤醒App执行场景。或者更可靠的做法:定时规则上传到服务端,服务端到时间推送通知给App,App收到通知后执行场景。

2. 事件触发的死循环

场景A:温度>28° → 开空调。场景B:空调开了 → 开窗。场景C:窗户开了 → 温度下降 → 关空调 → 温度上升 → 又触发场景A......

如果不做循环检测,场景之间可以无限触发,设备疯狂开关,直到烧坏。

解法: 加冷却时间。同一场景在执行后的一段时间内(如5分钟)不会再次触发。同时检测循环依赖------如果场景A的触发条件可能被场景B的动作影响,且场景B的触发条件可能被场景A的动作影响,就标记为潜在循环,提示用户。

3. 条件的时间窗口

"温度>28°开空调"------温度传感器每秒上报一次,28.1°、28.2°、28.3°......每次都触发场景?空调被反复开关?

解法: 条件加持续时间。温度持续>28°超过3分钟才触发。这样温度短暂波动不会误触发。实现方式:记录条件首次满足的时间,如果持续满足超过阈值才执行场景。

4. 动作执行失败的处理

场景有5个动作,第3个失败了------前2个已经执行,后2个还没执行。怎么办?

解法: 不做回滚。场景动作不是事务,部分成功也是成功。失败的动作用重试机制(最多3次),3次都失败就跳过,记录日志。下次场景触发时,失败的设备可能已经恢复了。

5. 场景规则的持久化

场景规则存在内存里,App重启就没了。用户辛辛苦苦配了10个场景,重启后全没了,你猜他会不会骂你?

解法: 场景规则必须持久化。用鸿蒙的@ohos.data.relationalStore(关系型数据库)存储规则,App启动时从数据库加载并注册到场景引擎。同时上传到云端,换设备也能恢复。

HarmonyOS 6适配说明

HarmonyOS 6对场景联动做了几项增强:

  1. 后台任务调度增强reminderAgentManager新增ReminderRequestTimer类型,支持精确到秒的定时触发,不再只有分钟级精度。定时场景可以更精确地执行。

  2. 地理围栏API :新增@ohos.geoLocationManager的地理围栏能力,支持进入/离开指定区域触发回调。场景联动可以直接使用地理围栏作为触发条件。

  3. 场景模板:HarmonyOS 6的智能家居Kit新增场景模板API,预置了"回家模式""离家模式""睡眠模式"等常见场景模板,用户一键套用,不用从零配置。

  4. 场景冲突可视化:新增场景冲突检测API,创建场景时自动检测与已有场景的冲突,返回冲突列表和冲突原因。

适配代码示例:

typescript 复制代码
// HarmonyOS 6 地理围栏触发
import { geoLocationManager } from '@kit.LocationKit'

// 创建地理围栏
const geofence: geoLocationManager.Geofence = {
  latitude: 39.9042,   // 家的纬度
  longitude: 116.4074, // 家的经度
  radius: 200,         // 200米范围
  expiration: 0        // 永不过期
}

const request: geoLocationManager.GeofenceRequest = {
  geofences: [geofence]
}

// 监听地理围栏事件
geoLocationManager.on('geofenceStatusChange', (event: geoLocationManager.GeofenceTransitionEvent) => {
  if (event.transition === geoLocationManager.GeofenceTransition.GEOFENCE_TRANSITION_ENTER) {
    // 进入围栏 → 触发回家模式
    SceneEngine.executeScene('scene-home', 'geofence')
  } else if (event.transition === geoLocationManager.GeofenceTransition.GEOFENCE_TRANSITION_EXIT) {
    // 离开围栏 → 触发离家模式
    SceneEngine.executeScene('scene-away', 'geofence')
  }
})

总结

场景联动是智能家居从"遥控器"进化到"管家"的关键。没有场景联动,用户就是一个人肉定时器,每天重复同样的操作。有了场景联动,系统自动根据条件和时间执行动作,用户只需要享受结果。

但场景联动不是"配几个规则就完事"的。冲突检测、循环防护、冷却时间、失败处理------这些细节决定了场景联动是"智能"还是"智障"。

维度 评价
学习难度 ⭐⭐⭐⭐ 条件-动作模型不难,但冲突检测、循环防护、后台保活需要深入设计
使用频率 ⭐⭐⭐⭐ 场景联动是智能家居的核心卖点,用户使用频率很高
重要程度 ⭐⭐⭐⭐⭐ 没有场景联动的智能家居只是"远程遥控",不是真正的智能

一句话:场景联动做得好,用户离不开;做得差,用户关掉不用。 差别就在那些细节里。