从闲置平板到家里的“控制大脑“:鸿蒙智慧中控面板完整实战

你家里有没有一台吃灰的旧平板?插上电往墙上一挂,它就能变成整个家的智能控制中心------灯、空调、窗帘、门锁、摄像头,一屏掌控。这不是需要花几百上千买的商业中控屏,用鸿蒙几行代码你就能自己做一个。


一、为什么我要自己做一个中控面板?

做这个项目之前,我先问了自己几个问题:

1.1 现有方案的三个痛点

市面上其实有很多智能家居中控方案,米家、HomeKit、涂鸦、Aqara都有自己的面板,但用下来有几个共同的问题:

  • 被生态锁死:买了米家的面板,就很难控制HomeKit的设备;反过来也一样。我家就是混合生态------客厅灯是Yeelight、空调是美的、门锁是鹿客、窗帘是Aqara、热水器是海尔,每装一个设备就要多一个APP。

  • 面板价格离谱:专门的中控屏从几百到上千不等,很多功能手机上都有,只是"挂墙上"这个形态就要多收这么多钱,我觉得不值。

  • 闲置设备浪费:抽屉里躺着一台MatePad 11,骁龙865性能够用、屏幕素质不错,就是平时不怎么用。与其让它吃灰,不如挂墙上当24小时常驻的控制中心。

鸿蒙的分布式能力天然适合解决这个问题:它不要求所有设备都是鸿蒙生态,通过HTTP/websocket/MQTT对接第三方平台API,再通过统一的UI层做聚合,就能做成"一个面板管全家"。

1.2 这个面板到底要做什么?

动笔写代码之前,一定要先把边界划清楚。我给自己列了"必须有"和"不做"两张清单:

✅ 必须有:

  1. 家里所有灯光的分组控制(客厅、卧室、书房、厨房、卫生间),支持亮度、色温调节
  2. 空调、地暖、新风的温度、模式、风速控制
  3. 窗帘开合度控制,支持全开、全关、指定百分比
  4. 门锁状态实时显示,开锁记录推送
  5. 摄像头实时预览画面
  6. 一键场景模式:回家、离家、观影、睡眠、起夜
  7. 24小时常驻,有人靠近自动亮屏,离开自动熄屏省电
  8. 手机、平板、手表都能控制,状态实时同步

❌ 一开始不做:

  1. 不自己做硬件接入层------先通过各平台公开的HTTP API/局域网协议对接,本地直连后面再迭代
  2. 不做语音控制------系统自带小艺足够用,重复造轮子意义不大
  3. 不做复杂的自动化规则引擎------第一版只做手动控制+定时场景,自动化后面再加
  4. 不做家庭权限管理------第一版单用户用,家庭成员权限后面再扩展

划清楚边界很重要,否则项目很容易越做越大、永远做不完。先做最小可用版本,再慢慢迭代。


二、整体架构设计:为什么这样分层?

2.1 三层架构

我把整个系统分成了三层,每层职责单一,后面替换其中任何一层都不影响其他:

┌─────────────────────────────────────────────────────┐

│ UI 展示层(多端) │

│ 平板横屏中控 / 手机竖屏控制 / 手表快捷卡片 / 车机入口 │

├─────────────────────────────────────────────────────┤

│ 业务逻辑层 │

│ 设备状态管理 / 场景编排 / 自动化引擎 / 权限控制 │

├─────────────────────────────────────────────────────┤

│ 设备接入层 │

│ 米家适配 / HomeKit适配 / MQTT直连 / 涂鸦云 / 虚拟设备 │

└─────────────────────────────────────────────────────┘

为什么这么分?

  • UI层只负责"显示什么"和"用户点了什么",不关心设备是哪个品牌的
  • 业务层是核心大脑,维护设备的统一抽象状态------灯就是灯,不管是米家还是Aqara的,在业务层都有同样的"开/关/亮度/色温"属性
  • 接入层负责把不同品牌的API转换成统一的内部格式,后面加新品牌只需要加一个适配器,不用改UI和业务逻辑

这是最典型的"适配器模式",但是做智能家居这个场景特别重要------你永远不知道以后会买什么品牌的设备,如果一开始就把某家API写死在UI代码里,后面改起来就是灾难。

2.2 统一设备模型设计

所有设备,不管是什么品牌、什么类型,都抽象成同一个基础结构:

// 所有设备的公共基类

interface BaseDevice {

id: string; // 全局唯一ID,格式:品牌:类型:序列号,比如"mi:light:0x1234"

name: string; // 用户自定义名称,比如"客厅主灯"

room: string; // 所在房间:living/bedroom/study/kitchen/bathroom

type: DeviceType; // 设备类型枚举

online: boolean; // 是否在线,离线设备灰显

lastUpdate: number; // 最后状态更新时间戳

brand: string; // 品牌标识,用于UI上显示小logo

}

// 设备类型枚举------所有支持的品类

enum DeviceType {

LIGHT = 'light', // 灯

AC = 'ac', // 空调

CURTAIN = 'curtain', // 窗帘

DOOR_LOCK = 'door_lock', // 门锁

CAMERA = 'camera', // 摄像头

SENSOR = 'sensor', // 传感器(温湿度/人体/门窗)

SWITCH = 'switch', // 智能插座/开关

SPEAKER = 'speaker', // 音箱

}

然后每个具体类型在基类基础上扩展自己的属性:

// 灯的扩展属性

interface LightDevice extends BaseDevice {

type: DeviceType.LIGHT;

power: boolean; // 开关

brightness: number; // 亮度 0-100

colorTemp?: number; // 色温 2700-6500K,支持调光的灯才有

color?: string; // RGB颜色,彩灯才有

}

// 空调的扩展属性

interface AcDevice extends BaseDevice {

type: DeviceType.AC;

power: boolean;

temperature: number; // 当前设定温度 16-30

mode: 'cool' | 'heat' | 'auto' | 'dry' | 'fan'; // 制冷/制热/自动/除湿/送风

fanSpeed: 'low' | 'mid' | 'high' | 'auto'; // 风速

currentTemp?: number; // 室温,有传感器的空调才支持

}

// 窗帘的扩展属性

interface CurtainDevice extends BaseDevice {

type: DeviceType.CURTAIN;

position: number; // 开合度 0=全关 100=全开

moving: boolean; // 是否正在运行中,运行中显示进度条

}

为什么要做这层抽象? 因为UI层面对"灯"的操作是固定的:一个开关、一个亮度滑条、一个色温调节。如果用户换了个品牌的灯,只要新适配器输出同样的LightDevice结构,UI一行代码都不用改。

如果不做这层抽象,直接在UI里调米家API,那后面加Aqara灯的时候就要把所有相关组件再写一遍,这是新手最容易犯的错误。

2.3 多HAP工程结构

按三层架构拆分模块,主HAP放UI,各设备适配层做成独立HAR包,业务核心逻辑也做成独立HAR:

SmartHomePanel/

├── entry/ # 主入口HAP

│ └── src/main/ets/

│ ├── entryability/

│ ├── pages/ # 所有页面

│ │ ├── MainPanel.ets # 平板横屏主面板(中控核心页面)

│ │ ├── PhoneHome.ets # 手机端首页

│ │ ├── DeviceDetail.ets # 设备详情调节页

│ │ ├── SceneEdit.ets # 场景编辑页

│ │ └── CameraView.ets # 摄像头预览页

│ └── components/ # 通用组件

│ ├── DeviceCard.ets # 设备卡片组件

│ ├── RoomTab.ets # 房间切换Tab

│ ├── SceneButton.ets # 场景大按钮

│ ├── LightSlider.ets # 灯亮度色温滑条

│ └── ClimateControl.ets # 空调地暖控制盘

├── smart-core/ # 业务核心HAR

│ └── src/main/ets/

│ ├── model/ # 统一设备模型定义

│ ├── manager/

│ │ ├── DeviceManager.ets # 设备状态中心

│ │ ├── SceneManager.ets # 场景管理器

│ │ └── RoomManager.ets # 房间分组管理

│ └── store/

│ └── AppState.ets # AppStorage全局状态

└── smart-adapters/ # 设备接入层HAR

└── src/main/ets/

├── BaseAdapter.ets # 适配器基类

├── MiHomeAdapter.ets # 米家适配

├── AqaraAdapter.ets // Aqara适配

├── MqttAdapter.ets // 通用MQTT设备直连

└── VirtualAdapter.ets // 虚拟设备,用于没有硬件时调试

这样拆分的好处是:开发调试的时候用VirtualAdapter生成一些假设备,UI和业务逻辑可以完全不依赖真实硬件,先跑通整个流程;等真实设备对接的时候只需要写适配器,前面的代码一行不改。


三、常驻中控面板:平板横屏布局的设计思考

挂在墙上的中控面板是这个项目的核心形态,也是用户用得最多的界面。这个页面我前后改了四版布局,这里把每版的思考过程写出来。

3.1 第一版:网格铺满------为什么不行?

最开始想的很简单:所有设备做成一个个方形卡片,按房间分Tab,网格铺满整个屏幕。类似手机上米家APP的首页,只是放大到平板尺寸。

做出来之后发现三个问题:

  1. 常用设备找起来慢:网格里混着灯、空调、窗帘、传感器,每次开客厅灯要扫一眼才能找到,不如物理开关快
  2. 信息密度不均匀:空调卡片需要显示温度、模式、风速,塞在小卡片里挤得慌;门窗传感器只需要显示"开/关"两个字,卡片空一大片
  3. 没有上下文感:用户站在面板前,他大概率是要控制"当前房间"的设备,而不是翻遍所有房间

3.2 第二版:分区域布局------方向对了,但还不够

第二版我把屏幕分成了左右两大块:

  • 左侧1/4:房间列表,当前房间高亮
  • 右侧3/4:当前房间的设备卡片,按类型分区(照明在上、环境在中、安防在下)

体验好了很多,但有个问题:有些操作是跨房间的。比如"观影模式"会关客厅灯、拉客厅窗帘、开客厅空调,但"睡眠模式"是关所有灯、检查所有门锁、开卧室夜灯------这种全局场景放在哪里?我一开始放在顶部工具栏,结果发现工具栏塞不下,而且太小,在墙上远远的点不到。

3.3 最终版:三区布局

第三版也就是现在用的版本,把屏幕分成了三个区域:

┌────────────────────────────────────────────────────────────┐

│ 🏠 回家 🎬 观影 🌙 睡眠 🚪 离家 💡 全关 [状态] │ ← 顶部场景栏(高度80vp)

├────────┬───────────────────────────────────────────────────┤

│ │ 💡 照明 │

│ 客厅 │ [主灯]● [射灯]○ [灯带]● [落地灯]○ [阳台灯]● │

│卧室 ├───────────────────────────────────────────────────┤

│书房 │ 🌡️ 环境 │

│厨房 │ 空调: 26°C 制冷 中速 新风: 2档 地暖: 关 │

│卫生间 ├───────────────────────────────────────────────────┤

│全部 │ 🪟 窗帘 │

│ │ 客厅窗帘: ████████░░ 80% 卧室窗帘: ░░░░░░░░ 0% │

│ ├───────────────────────────────────────────────────┤

│ │ 📹 安防 │

│ │ 门锁: 已锁 摄像头: 4路在线 门窗: 全部关闭 │

└────────┴───────────────────────────────────────────────────┘

1/5宽 4/5宽:设备按类型分组分区

为什么这么布局? 几个关键思考点:

  1. 场景栏放在最顶部,做成大按钮:场景操作是最高频的,"回家/离家/睡眠/观影"这几个按钮用户每天要点好几次,要大、要醒目、要点起来不费劲。每个按钮至少80vp高、120vp宽,手指按上去不会误触旁边的。

  2. 左侧房间栏常驻,但做窄:1/5宽度刚好够显示房间名+图标,当前房间用高亮背景+左侧色条标记。切换房间点一下就好,不需要额外操作。

  3. 右侧设备不做等大卡片,而是按类型分区流水布局:照明区每个灯就是一个横向的芯片式按钮(开着的高亮、关着的灰显),点一下开关、长按进详情调亮度;环境区显示温度风速数值;窗帘区显示进度条;安防区显示状态文字------不同类型设备用最适合它的交互控件,而不是硬塞进统一的方卡片里。

  4. 最常用的灯放在最上面:用户对灯的操作频率远高于其他设备,一进页面第一眼就能看到、手伸上去就能点到。

3.4 布局代码实现

用GridRow栅格系统做自适应,平板横屏是三栏结构,手机竖屏自动切换为单栏结构:

@Entry

@Component

struct MainPanel {

@StorageLink('currentRoom') currentRoom: string = 'living';

@StorageLink('devices') devices: Device[] = [];

@StorageLink('scenes') scenes: Scene[] = [];

// 根据当前房间过滤设备,按类型分组

private getRoomDevices() {

const roomDevices = this.devices.filter(d => d.room === this.currentRoom);

return {

lights: roomDevices.filter(d => d.type === DeviceType.LIGHT),

climates: roomDevices.filter(d => [DeviceType.AC, DeviceType.FAN, DeviceType.HEATER].includes(d.type)),

curtains: roomDevices.filter(d => d.type === DeviceType.CURTAIN),

security: roomDevices.filter(d => [DeviceType.DOOR_LOCK, DeviceType.CAMERA, DeviceType.SENSOR].includes(d.type)),

};

}

build() {

Column() {

// 1. 顶部场景栏

this.SceneBar()

// 2. 主体区域:左右分栏

GridRow({ columns: { sm: 4, md: 12, lg: 12 }, gutter: 12 }) {

GridCol({ span: { sm: 4, md: 12, lg: 2 } }) {

this.RoomSidebar() // 左侧房间栏:大屏占2列,小屏占满整行

}

GridCol({ span: { sm: 4, md: 12, lg: 10 } }) {

this.DeviceArea() // 右侧设备区:大屏占10列

}

}

.layoutWeight(1)

.padding(16)

}

.width('100%')

.height('100%')

.backgroundColor('#F5F5F5')

}

这里的断点设计:

  • sm(手机竖屏<600vp):房间栏放在设备区上面,占满整行,变成上下结构
  • md(手机横屏/小平板<840vp):和sm类似,但间距更大
  • lg(平板/大屏>=840vp):左右分栏,房间在左、设备在右

这样一套代码,手机和平板都能用,不需要写两个页面。

3.5 暗黑色模式默认开启

中控面板大概率是挂在客厅墙上的,晚上亮着个白屏幕会很刺眼,所以默认用暗黑模式:

// EntryAbility的onWindowStageCreate里设置

windowStage.loadContent('pages/MainPanel', (err) => {

const windowClass = windowStage.getMainWindowSync();

// 中控屏默认暗黑模式

windowClass.setWindowSystemBarProperties({

statusBarColor: '#121212',

navigationBarColor: '#121212',

isStatusBarLightIcon: true,

isNavigationBarLightIcon: true

});

});

暗黑模式下的颜色也要仔细调:

  • 背景用#121212,不要纯黑------纯黑在OLED屏幕上虽然省电,但视觉上会太死,卡片浮起来的层次感出不来
  • 卡片用#1E1E1E,比背景亮一个层级
  • 开启状态的设备用品牌色高亮(比如灯开着显示暖黄色#FFB74D),关闭状态用#424242灰色
  • 文字主色白色#FFFFFF,次要文字#B0B0B0,禁用文字#616161

四、设备状态管理:如何做到"多端状态实时同步"?

这是整个项目的技术核心。你在面板上点开灯,手机上要同步显示"开了";你在手机上把灯关了,面板上也要立刻变成关的状态;甚至你用物理开关按了灯,面板也要能感知到。

4.1 单例设备管理器

整个应用全局只应该有一个设备状态的"真相来源"(Single Source of Truth),所有页面、所有组件都从这同一个地方读状态,控制命令也都发到这同一个地方。绝不能每个组件自己维护一份设备状态,否则一定会出现"这里显示开、那里显示关"的不一致。

// 用单例模式实现全局设备管理器

class DeviceManager {

private static instance: DeviceManager;

private _devices: Map<string, Device> = new Map(); // id -> 设备对象

private _listeners: Set<(devices: Device[]) => void> = new Set();

private adapters: BaseAdapter[] = []; // 所有已注册的适配器

private constructor() {

// 私有构造函数,防止外部new

}

public static getInstance(): DeviceManager {

if (!DeviceManager.instance) {

DeviceManager.instance = new DeviceManager();

}

return DeviceManager.instance;

}

// 注册适配器,启动时调用

public registerAdapter(adapter: BaseAdapter) {

this.adapters.push(adapter);

// 监听适配器的状态变更事件

adapter.onDeviceStateChange((deviceId, state) => {

this.updateDeviceState(deviceId, state);

});

}

// 初始化:连接所有平台,拉取初始设备列表

public async init() {

for (const adapter of this.adapters) {

try {

await adapter.connect();

const devices = await adapter.discoverDevices();

devices.forEach(d => this._devices.set(d.id, d));

} catch (e) {

console.error(`适配器${adapter.name}连接失败`, e);

// 一个适配器失败不影响其他的,继续初始化

}

}

this.notifyListeners();

}

// 控制设备:UI层调用这个方法

public async controlDevice(deviceId: string, command: Partial<DeviceCommand>) {

const device = this._devices.get(deviceId);

if (!device || !device.online) return;

// 1. 先乐观更新UI------立刻在本地改状态,让用户感觉不到延迟

const oldState = { ...device };

this.updateDeviceState(deviceId, command);

try {

// 2. 找对应适配器发送真实命令

const brand = device.id.split(':')[0];

const adapter = this.adapters.find(a => a.brand === brand);

await adapter.sendCommand(deviceId, command);

// 3. 命令成功:以设备真实回报状态为准(有些设备执行完状态会有偏差)

const realState = await adapter.getState(deviceId);

this.updateDeviceState(deviceId, realState);

} catch (e) {

// 4. 命令失败:回滚到之前的状态,给用户提示

this.updateDeviceState(deviceId, oldState);

// 这里应该弹出一个Toast提示"控制失败,请检查设备"

console.error(`控制设备${deviceId}失败`, e);

}

}

// 更新设备状态,并通知所有监听者

private updateDeviceState(deviceId: string, partial: Partial<Device>) {

const old = this._devices.get(deviceId);

if (!old) return;

this._devices.set(deviceId, { ...old, ...partial, lastUpdate: Date.now() });

this.notifyListeners();

}

// 订阅状态变化,组件挂载时注册、卸载时取消

public subscribe(listener: (devices: Device[]) => void) {

this._listeners.add(listener);

return () => this._listeners.delete(listener); // 返回取消订阅函数

}

private notifyListeners() {

const all = Array.from(this._devices.values());

this._listeners.forEach(l => l(all));

}

}

这里有几个关键设计决策要讲清楚:

第一,为什么要用"乐观更新"?如果你等适配器把命令发给设备、设备回报执行成功、再更新UI,那整个过程可能要几百毫秒------用户点一下开关,过0.5秒灯的状态才变,会感觉"卡了、没反应"。乐观更新就是先假设命令会成功,立刻更新UI状态,后台异步发真实命令;如果失败了再回滚状态+提示。这个体验上的差别是巨大的,所有做智能家居控制的APP都会这么做,但很多新手开发者想不到。

第二,为什么命令失败要回滚?因为乐观更新本质是"赌命令会成功",但网络确实可能断、设备确实可能离线。如果失败了不回滚,UI上显示"灯开了",实际灯是关的,用户下次再来的时候看到的状态就是错的,会失去对产品的信任。

第三,为什么要用订阅模式而不是AppStorage直接存数组 ?AppStorage适合存简单状态,但设备列表是会频繁增量更新的。如果每次一个设备状态变了就整个数组替换,所有依赖@StorageLink('devices')的组件都会整棵重新渲染,设备多了性能会差。用订阅模式+组件内细粒度更新,性能好很多。

不过为了简化,我实际项目里还是两层结合:全局用AppStorage存设备列表保证跨页面同步,组件内部用@State订阅局部变更优化性能。

4.2 状态持久化:APP重启不丢状态

网络差的时候,APP启动可能拉不到最新设备状态,这时候应该显示上一次缓存的状态,而不是空白。用用户首选项持久化:

import preferences from '@ohos.data.preferences';

class StatePersistence {

private static pref: preferences.Preferences | null = null;

static async init(context: Context) {

this.pref = await preferences.getPreferences(context, 'device_state_cache');

}

// 每次状态变更后,1秒防抖写入缓存

static saveDevices(devices: Device[]) {

clearTimeout(this.saveTimer);

this.saveTimer = setTimeout(() => {

this.pref?.put('devices', JSON.stringify(devices));

}, 1000);

}

// 启动时读取缓存

static loadDevices(): Device[] {

const cached = this.pref?.get('devices', '[]') as string;

return JSON.parse(cached);

}

}

注意加了1秒防抖------因为设备状态更新很频繁(比如滑条调亮度会连续触发更新),每次都写磁盘会伤闪存、也影响性能,攒1秒批量写一次就够了。

4.3 多端分布式状态同步

平板面板和手机是两个独立的设备,它们之间的状态同步用鸿蒙分布式数据服务:

import distributedKVStore from '@ohos.data.distributedKVStore';

class DistributedSync {

private kvManager: distributedKVStore.KVManager | null = null;

private kvStore: distributedKVStore.SingleKVStore | null = null;

async init(context: Context) {

const config: distributedKVStore.KVManagerConfig = {

context,

bundleName: 'com.smarthome.panel'

};

this.kvManager = distributedKVStore.createKVManager(config);

const options: distributedKVStore.Options = {

createIfMissing: true,

encrypt: false,

backup: false,

autoSync: true, // 自动同步到同账号其他设备

kvStoreType: distributedKVStore.KVStoreType.SINGLE_VERSION,

securityLevel: distributedKVStore.SecurityLevel.S1

};

this.kvStore = await this.kvManager.getKVStore('device_state', options);

// 订阅远端数据变更------手机上改了设备状态,平板上自动更新

this.kvStore.on('dataChange', distributedKVStore.SubscribeType.SUBSCRIBE_TYPE_ALL, (data) => {

data.insertEntries?.forEach(entry => {

try {

const device = JSON.parse(entry.value.toString());

DeviceManager.getInstance().updateLocalState(device);

} catch(e) {}

});

});

}

// 本地设备状态变化时,同步到其他端

async syncDeviceState(device: Device) {

await this.kvStore?.put(`device:${device.id}`, JSON.stringify(device));

}

}

这样你在手机上开了灯,平板面板上不用刷新,1秒内就会自动变成开的状态;反过来也一样。不需要自己搭服务器,鸿蒙分布式数据服务在同一个华为账号下的设备之间自动P2P同步。


五、设备控制交互:每个控件的设计都有讲究

设备控制的交互看起来简单,就是"开关、滑条、按钮",但每一个控件放到"墙上中控"这个场景里,都有特殊的设计考量。

5.1 灯光开关:为什么不用Toggle开关?

最开始我用的是标准的Toggle开关组件,点一下开、再点一下关。但实际装到墙上用的时候发现问题:

  • Toggle开关很小,手指按上去经常点不准
  • Toggle只有开关,看不到亮度信息,要调亮度必须点进详情页
  • 一排Toggle看起来很像"设置页",不像"控制面板"

后来改成了大芯片按钮:每个灯是一个圆角矩形按钮,宽度根据屏幕自适应,里面左边是灯的图标和名称,右边是亮度小圆环,开着的按钮用亮色背景、关着的用深色背景,点一下整个按钮任意位置都能开关,长按0.5秒弹出亮度调节浮层。

@Component

export struct LightChip {

@ObjectLink device: LightDevice;

@State showBrightnessPopup: boolean = false;

private longPressTimer: number = -1;

build() {

Column() {

Row() {

Image(this.device.power ? $r('app.media.light_on') : $r('app.media.light_off'))

.width(24)

.height(24)

.fillColor(this.device.power ? '#FFB74D' : '#616161')

Text(this.device.name)

.fontSize(16)

.fontColor(this.device.power ? '#FFFFFF' : '#9E9E9E')

.margin({ left: 8 })

.layoutWeight(1)

// 亮度指示小圆环

if (this.device.power) {

Stack() {

Circle().width(32).height(32).fill('#333333')

Circle()

.width(32)

.height(32)

.fill('#FFB74D')

.opacity(this.device.brightness / 100)

Text(`${this.device.brightness}%`)

.fontSize(10)

.fontColor('#FFFFFF')

}

}

}

.padding(12)

.width('100%')

}

.backgroundColor(this.device.power ? '#2A2A2A' : '#1A1A1A')

.borderRadius(12)

.border({ width: this.device.power ? 1 : 0, color: '#FFB74D33' })

.onClick(() => {

// 点击:切换开关

DeviceManager.getInstance().controlDevice(this.device.id, {

power: !this.device.power

});

})

.onTouch((event: TouchEvent) => {

// 处理长按弹出亮度调节

if (event.type === TouchType.Down) {

this.longPressTimer = setTimeout(() => {

this.showBrightnessPopup = true;

}, 500);

} else if (event.type === TouchType.Up || event.type === TouchType.Cancel) {

clearTimeout(this.longPressTimer);

}

})

.bindSheet(this.showBrightnessPopup, this.BrightnessPopup(), {

height: 300,

dragBar: true,

onDisappear: () => this.showBrightnessPopup = false

})

}

@Builder

BrightnessPopup() {

Column({ space: 20 }) {

Text('调节亮度')

.fontSize(18)

.fontWeight(FontWeight.Medium)

Slider({

value: this.device.brightness,

min: 1,

max: 100,

step: 1,

style: SliderStyle.OutSet

})

.blockColor('#FFB74D')

.selectedColor('#FFB74D')

.width('100%')

.onChange((value: number) => {

// 滑条拖动时实时发送命令,但做节流处理

this.throttledControl({ brightness: Math.round(value) });

})

if (this.device.colorTemp) {

Text('调节色温')

.fontSize(18)

.margin({ top: 16 })

Slider({

value: this.device.colorTemp,

min: 2700,

max: 6500,

step: 100,

})

.onChange((value: number) => {

this.throttledControl({ colorTemp: Math.round(value) });

})

}

}

.padding(24)

}

// 节流:滑条拖动过程中100ms发一次命令,不要拖一点发一次

private throttledControl = throttle((cmd) => {

DeviceManager.getInstance().controlDevice(this.device.id, cmd);

}, 100);

}

这里有个重要细节:滑条控制一定要做节流。用户拖动亮度滑条的时候,onChange事件会以非常高的频率触发(可能几十毫秒一次),如果每次都给设备发命令,一者网络扛不住,二者设备接收太频繁会卡顿、甚至掉线。100ms发一次是比较合理的间隔,用户感觉不到延迟,设备也能跟得上。

5.2 空调控制:温度加减按钮要大

空调控制最核心的操作就是"加减温度",这个按钮一定要做的足够大:

@Component

export struct AcControl {

@ObjectLink device: AcDevice;

build() {

Row({ space: 24 }) {

// 温度减

Button('-')

.fontSize(32)

.width(60)

.height(60)

.borderRadius(30)

.backgroundColor('#333333')

.onClick(() => {

if (this.device.temperature > 16) {

DeviceManager.getInstance().controlDevice(this.device.id, {

temperature: this.device.temperature - 1

});

}

})

// 当前温度大数字显示

Column() {

Text(`${this.device.temperature}`)

.fontSize(64)

.fontWeight(FontWeight.Bold)

.fontColor('#FFFFFF')

Text('°C')

.fontSize(20)

.fontColor('#9E9E9E')

.margin({ top: -8 })

}

// 温度加

Button('+')

.fontSize(32)

.width(60)

.height(60)

.borderRadius(30)

.backgroundColor('#333333')

.onClick(() => {

if (this.device.temperature < 30) {

DeviceManager.getInstance().controlDevice(this.device.id, {

temperature: this.device.temperature + 1

});

}

})

}

// 模式切换横排按钮

Row({ space: 8 }) {

ForEach(['cool', 'heat', 'auto', 'dry', 'fan'], (mode) => {

Button(this.modeText(mode))

.fontSize(14)

.height(36)

.backgroundColor(this.device.mode === mode ? '#2196F3' : '#333333')

.onClick(() => {

DeviceManager.getInstance().controlDevice(this.device.id, { mode });

});

})

}

}

温度数字用64fp超大号字体,远远就能看到当前多少度;加减按钮60x60vp的大圆,点起来毫不费劲------这就是中控屏和手机APP的区别:手机是拿在手里凑到眼前看的,中控屏是挂在墙上,人可能站在一两米外看,字体和按钮都要大一号。

5.3 窗帘控制:进度条是最直观的

窗帘的开合度最适合用进度条:用户拖动到哪个位置,窗帘就开到哪个位置,比"点一下开/点一下关"的按钮体验好太多。

@Component

export struct CurtainSlider {

@ObjectLink device: CurtainDevice;

build() {

Row({ space: 16 }) {

Image($r('app.media.curtain'))

.width(24)

.height(24)

.fillColor(device.position > 0 ? '#4CAF50' : '#616161')

Text(device.name)

.fontSize(16)

.width(100)

Slider({

value: device.position,

min: 0,

max: 100,

step: 1

})

.layoutWeight(1)

.blockColor('#4CAF50')

.selectedColor('#4CAF50')

.onChange((value: number, mode: SliderChangeMode) => {

if (mode === SliderChangeMode.End) {

// 拖动结束再发命令,不要拖动过程中一直发------窗帘是机械结构,不能频繁换向

DeviceManager.getInstance().controlDevice(device.id, {

position: Math.round(value)

});

}

})

Text(`${device.position}%`)

.fontSize(14)

.width(50)

.textAlign(TextAlign.End)

}

.padding(12)

}

}

注意窗帘和灯光滑条的区别:

  • 灯光滑条拖动过程中节流发命令,因为灯是电子设备,可以连续变化
  • 窗帘滑条不要在拖动过程中发命令,只在拖动结束时发一次!因为窗帘是机械电机,如果拖到30%发一个命令,窗帘开始往30%走;拖到50%又发一个,电机突然换向,时间长了会烧坏电机。

这个细节很多人不知道,产品上线后坏了好几个窗帘电机才总结出来的。


六、一键场景:"回家模式"到底做了什么?

场景是智能家居最有魅力的部分------按一个按钮,多个设备同时动作,不用一个个去开。但场景的实现远不是"循环发几个命令"那么简单。

6.1 场景数据结构

interface Scene {

id: string;

name: string;

icon: ResourceStr;

color: string; // 按钮背景色

description: string;

actions: SceneAction[]; // 要执行的动作列表

confirmRequired?: boolean;// 是否需要二次确认(比如离家模式)

}

interface SceneAction {

deviceId: string;

command: Partial<DeviceCommand>;

delay?: number; // 执行延迟,毫秒,用于动作之间错开时间

}

每个场景就是一串有序的设备动作,每个动作可以指定延迟时间。

6.2 为什么动作之间要加延迟?

你可能会想:回家模式不就是"开灯+开空调+开窗帘",同时发三个命令不就行了?

实际不行,有两个原因:

  1. 很多智能家居网关有并发限制------比如米家网关一秒内最多接收3-5个命令,发多了会丢包、命令不执行
  2. 同时开多个大功率设备,电路有冲击------这个倒是小概率,但设计上留个心眼总是好的

所以动作之间要故意错开一点时间,间隔200-300毫秒一个,既不会让用户感觉到明显延迟,网关也能处理过来:

class SceneManager {

async executeScene(scene: Scene) {

console.log(`开始执行场景: ${scene.name}`);

// 按delay排序

const sorted = [...scene.actions].sort((a, b) => (a.delay || 0) - (b.delay || 0));

for (const action of sorted) {

// 等待到该动作的延迟时间

await new Promise(resolve => setTimeout(resolve, action.delay || 0));

// 发送命令,单个动作失败不影响其他动作继续执行

try {

await DeviceManager.getInstance().controlDevice(action.deviceId, action.command);

} catch (e) {

console.error(`场景动作失败: ${action.deviceId}`, e);

// 场景里一个设备失败不要中断整个场景,继续执行后面的

}

}

console.log(`场景执行完成: ${scene.name}`);

}

}

单个动作失败要不要中断整个场景?不要。 回家模式如果客厅灯坏了,空调该开还是要开,窗帘该拉还是要拉,不能因为一个设备出问题整个场景都停在那里。

6.3 几个核心场景的设计思路

回家模式(天暗时):

{

name: '回家',

icon: $r('app.media.scene_home'),

color: '#FFB74D',

actions: [

{ deviceId: 'mi:light:living_main', command: { power: true, brightness: 80 }, delay: 0 },

{ deviceId: 'aqara:curtain:living', command: { position: 100 }, delay: 200 }, // 拉开窗帘

{ deviceId: 'mi:ac:living', command: { power: true, temperature: 26, mode: 'auto' }, delay: 500 },

{ deviceId: 'mi:light:entrance', command: { power: true, brightness: 100 }, delay: 0 }, // 玄关灯立刻开

]

}

注意玄关灯delay是0,其他灯和空调有延迟------用户开门进来最先看到的就是玄关,玄关灯必须立刻亮;客厅灯、空调晚几百毫秒完全感知不到。

观影模式:

{

name: '观影',

icon: $r('app.media.scene_movie'),

color: '#7B1FA2',

actions: [

{ deviceId: 'mi:light:living_main', command: { power: false }, delay: 0 }, // 主灯关

{ deviceId: 'mi:light:living_strip', command: { power: true, brightness: 20 }, delay: 100 }, // 留氛围灯带

{ deviceId: 'aqara:curtain:living', command: { position: 0 }, delay: 300 }, // 关窗帘

{ deviceId: 'mi:ac:living', command: { fanSpeed: 'low' }, delay: 500 }, // 空调调静音

]

}

观影模式不是把所有灯都关掉,而是留一条低亮度的氛围灯带------全黑环境看投影/电视眼睛会累,20%亮度的背光刚好看得清路,又不影响画面。

睡眠模式:

{

name: '睡眠',

icon: $r('app.media.scene_sleep'),

color: '#3F51B5',

actions: [

{ deviceId: 'mi:light:all', command: { power: false }, delay: 0 }, // 所有灯关

{ deviceId: 'aqara:curtain:all', command: { position: 0 }, delay: 300 }, // 所有窗帘关

{ deviceId: 'mi:ac:bedroom', command: { power: true, temperature: 27, mode: 'cool', fanSpeed: 'low' }, delay: 500 },

{ deviceId: 'aqara:lock:door', command: { lock: true }, delay: 1000 }, // 检查门锁

]

}

睡眠模式有个特殊处理:如果发现门锁已经锁了,就不要重复发锁门命令;如果没锁,就自动锁上------给用户"我帮你检查好了"的安全感。

离家模式:为什么需要二次确认?因为离家模式会关所有灯、关空调、关窗帘、锁门------这些操作都是"不可轻易撤销"的,如果不小心误触了,人已经出门了发现空调被关了(夏天回家会很热),或者门锁了发现钥匙没带,体验会很差。所以离家模式点下去弹一个确认框"确定要离家吗?将关闭所有设备并锁门",用户点确认才执行。

6.4 场景执行反馈

场景执行的时候,一定要给用户明确的反馈,不要点了按钮安安静静什么反应都没有------用户会怀疑"到底有没有执行"。

我做的反馈是:点下场景按钮后,按钮先变成一个加载进度圈(进度按动作完成比例走),执行完成后按钮变成对号显示1秒,然后恢复原样。每个设备执行成功/失败在设备卡片上有个短暂的闪烁动效,让用户知道哪些设备动了。


七、常驻熄屏与人体感应:24小时在线的关键

中控面板是要24小时插电挂墙上的,一直亮屏既烧屏又费电,但熄屏了又要按电源键才能用,就失去了"中控"的意义。这个矛盾怎么解决?

7.1 亮屏策略:三层判断

我的方案是:有人靠近自动亮屏,没人自动熄屏;显示常亮性的极简时钟界面,不一直亮主界面。

具体策略分三层:

  1. 默认状态:熄屏显示极简时钟(AOD,Always On Display),只显示时间、日期、温度,整体亮度很低
  2. 有人靠近:通过超声波距离传感器(或平板前置摄像头的人体检测)感知到人走近了,立刻亮屏到主面板界面
  3. 操作超时:亮屏后30秒没人操作,自动回到AOD时钟界面

鸿蒙系统本身支持AOD能力,但第三方应用没法直接控制系统AOD,所以我自己实现了"应用内AOD":

@Component

struct MainPanel {

@State screenMode: 'active' | 'aod' | 'sleep' = 'active';

private lastInteractionTime: number = Date.now();

private idleCheckTimer: number = -1;

private wakeLock: window.WakeLock | null = null;

aboutToAppear() {

// 申请常亮锁,防止系统自动锁屏

window.getLastWindow(getContext(this)).then(win => {

win.setWindowKeepScreenOn(true);

});

// 启动空闲检测定时器,每5秒检查一次

this.idleCheckTimer = setInterval(() => {

const idleTime = Date.now() - this.lastInteractionTime;

if (idleTime > 30000 && this.screenMode === 'active') {

this.screenMode = 'aod'; // 30秒无操作进AOD

this.setScreenBrightness(0.1); // 把屏幕亮度调到最低

}

}, 5000);

// 启动人体感应检测

this.startProximityWatch();

}

// 监听距离传感器/人体检测

private startProximityWatch() {

// 鸿蒙人体感知能力:检测到人脸靠近时唤醒

import { humanDetection } from '@kit.SensingKit';

// 简化示意:实际实现中通过传感器或前置摄像头检测

sensor.on(sensor.SensorId.PROXIMITY, (data) => {

if (data.distance < 50) { // 50cm内检测到物体,认为有人靠近

this.wakeUpScreen();

}

});

}

private wakeUpScreen() {

this.lastInteractionTime = Date.now();

if (this.screenMode !== 'active') {

this.screenMode = 'active';

this.setScreenBrightness(-1); // -1表示恢复自动亮度

}

}

// 用户触摸屏幕任意位置,重置计时

onTouch(() => {

this.lastInteractionTime = Date.now();

if (this.screenMode === 'aod') {

this.screenMode = 'active';

this.setScreenBrightness(-1);

}

})

7.2 防烧屏处理

OLED屏幕长时间显示同一个静态画面会烧屏(像素老化留下永久残影),AOD界面必须做防烧屏处理:

@Component

struct AodClock {

@State offsetX: number = 0;

@State offsetY: number = 0;

aboutToAppear() {

// 每分钟随机移动一次时钟位置,防止同一像素长时间点亮

setInterval(() => {

this.offsetX = Math.random() * 40 - 20; // ±20vp随机偏移

this.offsetY = Math.random() * 20 - 10;

}, 60000);

}

build() {

Column() {

Text(this.currentTime)

.fontSize(72)

.fontColor('#FFFFFF')

.opacity(0.6)

Text(`${this.date} ${this.currentTemp}°C`)

.fontSize(16)

.fontColor('#9E9E9E')

.margin({ top: 8 })

}

.width('100%')

.height('100%')

.justifyContent(FlexAlign.Center)

.translate({ x: this.offsetX, y: this.offsetY }) // 随机偏移

.backgroundColor('#000000')

}

}

每分钟随机移动几个像素,肉眼几乎察觉不到,但能有效防止烧屏------这也是手机系统AOD的标准做法。

7.3 夜间自动调光

晚上睡觉的时候,即使是最低亮度的AOD,在全黑卧室里还是会觉得亮。所以加一个定时:晚上11点到早上7点,AOD亮度进一步降低,甚至可以关闭AOD、真正熄屏:

private checkNightMode() {

const hour = new Date().getHours();

if (hour >= 23 || hour < 7) {

// 夜间模式

if (this.screenMode === 'aod') {

this.setScreenBrightness(0.03); // 极低亮度,夜里不刺眼

}

}

}


八、摄像头预览:不要自己推流,用系统组件

安防摄像头的实时预览是个硬需求------门口有人按门铃,在面板上就能看到门口画面。一开始我想自己用RTSP拉流解码,后来发现鸿蒙已经有现成的分布式相机能力,根本不用自己做。

8.1 分布式摄像头调用

鸿蒙支持把其他设备的摄像头虚拟成本地摄像头,直接用XComponent渲染即可:

import camera from '@ohos.multimedia.camera';

@Component

export struct CameraPreview {

@Prop cameraId: string;

private cameraInput?: camera.CameraInput;

private previewOutput?: camera.PreviewOutput;

private cameraSession?: camera.CameraSession;

@State isOnline: boolean = false;

@State isLoading: boolean = true;

aboutToAppear() {

this.initCamera();

}

async initCamera() {

try {

const cameraManager = camera.getCameraManager(getContext(this));

// 枚举所有摄像头,包括分布式摄像头

const cameras = cameraManager.getSupportedCameras();

const targetCamera = cameras.find(c => c.connectionType === camera.ConnectionType.CAMERA_CONNECTION_TYPE_DISTRIBUTED);

if (!targetCamera) {

this.isOnline = false;

return;

}

this.isOnline = true;

this.cameraInput = cameraManager.createCameraInput(targetCamera);

await this.cameraInput.open();

const profile = targetCamera.cameraProfiles.find(p => p.size.width >= 1280);

const xComponentCtx = this.xComponentRef.getXComponentContext();

this.previewOutput = cameraManager.createPreviewOutput(profile, xComponentCtx.getSurfaceId());

this.cameraSession = cameraManager.createCameraSession();

await this.cameraSession.beginConfig();

this.cameraSession.addInput(this.cameraInput);

this.cameraSession.addOutput(this.previewOutput);

await this.cameraSession.commitConfig();

await this.cameraSession.start();

this.isLoading = false;

} catch (e) {

console.error('摄像头初始化失败', e);

this.isOnline = false;

}

}

build() {

Stack() {

XComponent({

id: 'camera_preview',

type: 'surface',

controller: this.xComponentController

})

.width('100%')

.height('100%')

if (this.isLoading) {

Progress({ type: ProgressType.Circular }).width(40)

}

if (!this.isOnline) {

Column() {

Image($r('app.media.camera_offline')).width(48).height(48)

Text('摄像头离线').fontColor('#9E9E9E').margin({ top: 8 })

}

}

}

.width('100%')

.aspectRatio(16 / 9)

.borderRadius(12)

.backgroundColor('#000000')

.clip(true)

}

}

如果摄像头不是鸿蒙智联的、是普通的RTSP网络摄像头,那就要用FFmpeg自己拉流解码了,这个比较复杂,建议第一版先对接鸿蒙智联摄像头,后续再扩展ONVIF/RTSP协议支持。


九、功耗优化:插电也不能浪费电

虽然中控面板是一直插电的,但功耗优化还是要做------不只是省电费,更重要的是平板长期插电高负载运行会发热、加速电池老化、缩短设备寿命。

9.1 功耗拆解

我用DevEco Profiler实测了一下,各个模块的耗电占比:

模块 功耗占比 说明
屏幕常亮 ~55% 最大头,所以AOD和亮度调节很重要
Wifi网络心跳 ~15% 维持和设备的长连接
传感器轮询 ~12% 人体感应、温湿度更新
UI渲染 ~10% 页面刷新、动画
CPU空闲 ~8% 后台休眠

有针对性地优化:

屏幕层面:

  • 30秒无操作自动进AOD,亮度降到10%
  • 夜间(23:00-7:00)AOD亮度进一步降到3%
  • AOD界面每分钟随机偏移防烧屏
  • 主界面用深色背景,OLED屏黑色像素不发光本身就省电

网络层面:

  • 设备状态轮询不要用高频轮询,用MQTT长连接+事件推送
  • 没有事件推送能力的第三方平台(比如米家云API),轮询间隔设为30秒,不要1秒轮一次
  • 后台时停止非必要的轮询,只保留门锁、传感器这类事件型推送

CPU层面:

  • AOD模式下,主界面的渲染完全暂停,不要在后台跑动画
  • 设备列表用LazyForEach懒加载,不渲染屏幕外的设备卡片
  • 状态更新防抖,100ms内多次更新合并成一次UI刷新

9.2 长期插电的电池保护

平板长期插着电用,电池一直保持100%满电状态,会加速电池老化。鸿蒙系统有电池保护模式,但自己代码里也能做一些事情:

import batteryInfo from '@ohos.batteryInfo';

// 监听电池状态,如果一直在充电且电量高于90%,建议提醒用户开启电池保护

batteryInfo.on('batteryChange', (info) => {

if (info.charging && info.level >= 90) {

// 记录连续满电时间,如果超过24小时提示用户

this.continuousFullChargeTime += 1;

if (this.continuousFullChargeTime > 24 * 60) {

// 弹出提示:"建议在系统设置中开启智能充电模式,延长电池寿命"

}

} else {

this.continuousFullChargeTime = 0;

}

});

更极致的做法是:如果设备一直插电,可以root或者通过系统API把充电上限限制在80%,但这需要系统权限,普通应用做不到,只能提醒用户手动开智能充电。


十、适配手机、手表、车机:不只是平板能用

中控面板是核心形态,但其他设备也要能用,只是场景不同:

  • 手机:用户在外面/沙发上不想站起来去面板跟前,用手机控制
  • 手表:躺床上不想拿手机,抬腕就能关灯
  • 车机:快到家的时候,在车上提前开空调

10.1 手机端适配

手机竖屏空间小,布局完全不同:

  • 顶部不做横排场景大按钮,改成大卡片轮播(Swiper),每个场景一屏宽
  • 不做左右分栏,房间切换用顶部横向Tab
  • 设备卡片更大、更疏,适合手指点
  • 每个设备单独一行,点击弹底部弹窗控制,不要像平板那样挤在一个页面

断点适配在前面布局代码里已经写了,sm断点下自动切换为手机布局。

10.2 手表端:极简卡片

手表屏幕极小,不能做复杂控制,只放最高频的三个功能:

  1. 三个最常用场景:回家、离家、睡眠------三个大按钮占满屏幕
  2. 当前房间的灯快速开关
  3. 门锁状态查看+开锁

不要尝试在手表上调温度、调窗帘,体验会很差,这些操作留给手机和平板。

10.3 车机端:只留一个"回家"按钮

用户在车机上对智能家居的唯一高频需求就是"快到家了提前开空调",所以车机端不需要做完整控制界面,只做一个万能卡片:显示"离家X公里,预计Y分钟到家",一个大按钮"提前开启回家模式"就够了。

鸿蒙万能卡片(FormAbility)天生适合这种场景:

// form_card.json 卡片配置

{

"name": "GoingHomeCard",

"description": "回家预热卡片",

"src": "./pages/GoingHomeCard",

"window": { "designWidth": 336, "autoDesignWidth": false },

"supportDimensions": ["2*2"],

"defaultDimension": "2*2"

}

卡片上实时显示离家距离和预计到家时间,点一下就触发回家模式------用户在车机主屏上点一下就行,不用进APP。


十一、上架审核:这些坑提前避

做这个应用的时候我前后被拒了三次,这些经验写出来让大家少走弯路:

11.1 权限问题是重灾区

这个应用需要的权限比较多,每一个都要写清楚用途,否则直接拒:

权限 用途说明怎么写才能过
ohos.permission.INTERNET 用于连接智能家居云平台,控制智能设备
ohos.permission.GET_WIFI_INFO 用于发现局域网内的智能设备(本地直连)
ohos.permission.KEEP_BACKGROUND_RUNNING 用于保持设备状态实时同步,响应手动控制
ohos.permission.CAMERA 可选:用于人体感应亮屏,用户可在设置中关闭
ohos.permission.DISTRIBUTED_DATASYNC 用于多设备间设备状态同步

注意:所有涉及用户隐私的权限,一定要在第一次使用前弹框说明为什么要这个权限,用户同意了再申请,不能一启动就把所有权限都申请了。尤其是相机权限,一定要明确告诉用户"仅用于靠近自动亮屏,不会上传任何画面",否则用户会担心被偷拍。

11.2 后台运行审核

中控类应用需要长驻后台,这在审核里是重点关注项,需要在申请后台权限的时候说明:

  • 为什么需要后台运行(实时接收设备状态变化、门锁异常报警推送)
  • 后台做了什么功耗优化(AOD策略、网络长连接优化、CPU休眠)
  • 给用户一个"退出后台运行"的开关,用户随时可以停掉

鸿蒙对后台应用的功耗要求越来越严,不要在后台做任何非必要的事情,否则很容易被系统杀掉,审核也过不了。

11.3 隐私合规要点

智能家居应用能拿到用户很多隐私数据:什么时候在家、什么时候出门、家里什么时候有人,这些数据非常敏感。审核时会查:

  • 有没有隐私政策弹窗
  • 用户数据(设备列表、操作记录)是不是只在本地存储,有没有上传到你自己的服务器
  • 如果上传了,有没有明确告知用户、有没有加密传输
  • 用户能不能一键删除所有数据

我的做法是:所有设备状态和操作记录默认只存在本地和鸿蒙分布式数据服务里,不经过第三方服务器,隐私审查一次就过了。如果你需要做云端服务(比如远程控制不在家的时候),一定要在隐私政策里写清楚哪些数据会上传、存在哪里、保存多久。


十二、踩坑总结:那些让我调试到半夜的问题

最后把整个开发过程中踩过的坑整理出来,希望你不用再踩一遍:

12.1 最容易踩的十个坑

问题描述 原因 解决方案
关灯后UI显示关了,过几秒又变成开的 适配器拉状态频率太高,乐观更新后被旧状态覆盖了 命令发送后3秒内忽略该设备的状态轮询结果,以命令返回为准
窗帘电机经常中途换向、咔哒响 滑条拖动过程中频繁发命令,电机频繁换向 滑条只在End事件发命令,拖动过程中不发
面板过夜后变卡、内存越来越大 WebSocket连接断了后重连没释放旧连接,内存泄漏 重连前一定要关闭旧socket,定期重启网络模块
AOD模式下半夜屏幕突然变亮 推送通知亮屏后没回到AOD亮度 监听系统屏幕亮屏事件,亮屏后5秒无操作自动回到AOD
空调设置26度,过一会跳回25度 空调自身有温度校准(比如设定26实际到25就停),回报状态是25 命令发送后短时间内忽略温度字段的回滚,只接受用户主动设置的值
平板上布局正常,手机上元素溢出 平板用了固定vp宽度,小屏幕装不下 所有布局用百分比/GridCol/自适应,不要写死固定宽度
分布式同步经常失效,两台设备状态不一致 KV存储同步需要同账号+同局域网+蓝牙开启,少一个条件就不行 做状态冲突解决机制:以时间戳最新的为准,用户手动操作优先级高于自动同步
场景执行到一半卡住 某个设备离线,await等待超时 每个设备控制加3秒超时,超时跳过继续下一个,不要无限等待
人体感应晚上不好用,白天乱亮屏 前置摄像头人脸检测夜间光线差、距离传感器被灰尘挡住 加环境光传感器判断:白天光线好才用人脸检测,晚上用距离传感器
平板插电半年后电池鼓包 长期100%满电高温加速老化 提醒用户开启智能充电模式,夏天注意设备散热,不要贴在不通风的墙面上

12.2 后续可以扩展的方向

这个项目第一版做到这里已经能用了,但还有很多可以迭代的空间:

  1. 自动化引擎:根据温度、人体感应、时间、地理位置自动触发场景(比如温度高于28度自动开空调、检测到人来自动亮玄关灯)
  2. 语音控制:接入鸿蒙小艺的自定义语音指令,说"我回来了"自动触发回家模式
  3. 能耗统计:统计每个设备每天/每月用电情况,给出节能建议
  4. 更多品牌适配器:华为智选、涂鸦、Home Assistant接入,支持更多设备
  5. 门锁摄像头联动:门锁被打开时自动抓拍照片、推送通知到手机
  6. 音乐控制:接入智能音箱,中控面板直接选歌、调音量

写在最后

做这个项目最大的感受是:鸿蒙的分布式能力不是炫技,它真的能解决实际生活里的小痛点。你不需要买新硬件,家里闲置的旧平板就能发挥余热;不需要被单一品牌生态绑定,通过分层架构自己做聚合;不需要等厂商出完美的产品,自己动手就能做一个最符合自己使用习惯的控制中心。

技术本身从来不酷,用技术把生活变方便一点,那才是酷的事情。

希望这篇实战记录能帮到你。如果你也做了自己的中控面板,欢迎分享你的经验------智能家居的乐趣,本来就在于折腾。

相关推荐
500841 小时前
React Native for OpenHarmony 实战:三方库 react-native-url-polyfill 的鸿蒙化适配指南
javascript·react native·react.js·性能优化·electron·harmonyos
accept 99%1 小时前
拆开Jev 的原理和本地跑法 Jev科普(二)
人工智能·机器学习
心理之旅1 小时前
WorkbuddyAI办公----- 不懂代码,也能给自己做一个自动化工具:9 轮对话实录
ai编程
johnsong1 小时前
125B模型装上桌面:AI推理主权革命与治理悖论
人工智能
库拉镜像AI牛牛1 小时前
漫剧工作室量产方案:依托知漫剧 AI 短剧降本增效
大数据·服务器·前端·人工智能·语音识别
狂野小白兔1 小时前
AI游戏制作04——Codex 搭配 Godot MCP 全流程教程
人工智能·游戏·godot
skywalk81631 小时前
deepseek harness 官方已经更新到新版:v0.2.1-alpha.1 Pre-release请把咱们的FreeBSD版本也同步更新到新版本!
人工智能·freebsd·实践·deepseek·harness
心理之旅1 小时前
从 40 分钟到 38 秒:SAP Business One 物料库存自动取数实战
ai编程
HelloWorld0012 小时前
告别轮询与断流!基于 Spring Boot 3 + SSE + Redis 打造生产级 Agent 流式思考与工具调用中枢
ai编程