Skip to content

Commit

Permalink
feat: 新增智能晾衣机 H1
Browse files Browse the repository at this point in the history
  • Loading branch information
baran.wang committed May 15, 2024
1 parent e0537a2 commit b81860f
Show file tree
Hide file tree
Showing 13 changed files with 435 additions and 548 deletions.
5 changes: 5 additions & 0 deletions .changeset/sweet-lizards-cry copy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"homebridge-plugin-aqara": minor
---

红外模拟电视遥控器
5 changes: 5 additions & 0 deletions .changeset/sweet-lizards-cry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"homebridge-plugin-aqara": minor
---

新增智能晾衣机 H1
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@

目前支持的设备有:

- [x] Aqara 智能晾衣机 Lite
- [x] Aqara 智能晾衣机Lite
- [x] Aqara 智能晾衣机H1(Aqara 的 API 暂不支持控制行程的百分比)
- [x] 红外模拟电视遥控器
671 changes: 149 additions & 522 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
],
"devDependencies": {
"@changesets/cli": "^2.27.1",
"@modern-js/module-tools": "^2.46.1",
"@modern-js/module-tools": "^2.49.3",
"@types/node": "^18.16.20",
"@typescript-eslint/eslint-plugin": "^5.62.0",
"@typescript-eslint/parser": "^5.62.0",
Expand Down
30 changes: 25 additions & 5 deletions src/accessories/base.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import type { Service } from 'homebridge';
import type { AqaraHomebridgePlatform } from '../platform';

type GenerateServicesParams =
| typeof Service
| {
service: typeof Service;
subName: string;
};
export class BaseAccessory {
public services: Service[] = [];
public services: Record<string, Service> = {};

constructor(readonly platform: AqaraHomebridgePlatform, readonly accessory: AqaraPlatformAccessory) {
const { deviceInfo } = this.accessory.context;
Expand All @@ -12,12 +18,22 @@ export class BaseAccessory {
.setCharacteristic(Characteristic.Manufacturer, 'Aqara')
.setCharacteristic(Characteristic.Model, deviceInfo.model)
.setCharacteristic(Characteristic.SerialNumber, deviceInfo.did.split('.').pop()!.toUpperCase());
this.init();
}

protected generateServices<T extends typeof Service>(services: T[]) {
this.services = services.map(
(service) => this.accessory.getService(service as any) || this.accessory.addService(service as any),
);
protected generateServices(services: Record<string, GenerateServicesParams>) {
this.services = Object.entries(services).reduce<Record<string, Service>>((acc, [key, params]) => {
const existingService = this.accessory.getService(key);
if (existingService) {
acc[key] = existingService;
} else if ('subName' in params) {
const { subName, service } = params;
acc[key] = this.accessory.addService(service, `${this.accessory.displayName} - ${subName}`, key);
} else {
acc[key] = this.accessory.addService(params, this.accessory.displayName, key);
}
return acc;
}, {});
}

protected getResourceValue(resourceId: string) {
Expand All @@ -38,4 +54,8 @@ export class BaseAccessory {
return Promise.reject(error);
}
}

init() {
// 暴露给子类
}
}
4 changes: 3 additions & 1 deletion src/accessories/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export * from './lumi.airer.acn02';
export * from './lumi.airer.acn02';
export * from './lumi.airer.acn001';
export * from './virtual.ir.tv';
98 changes: 98 additions & 0 deletions src/accessories/lumi.airer.acn001.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { BaseAccessory } from './base';

enum Operation {
Lightbulb = '4.21.85',
/** 风干 */
AirDry = '4.66.85',
/** 烘干 */
Dry = '4.67.85',
/** 消毒 */
Disinfection = '4.22.85',
/** 运动控制 */
Control = '14.1.85',
}

enum OpenClose {
Close = '0',
Open = '1',
}

export class LumiAirerAcn001 extends BaseAccessory {
init() {
const { Lightbulb, WindowCovering, Fanv2, Switch } = this.platform.Service;
this.generateServices({
lightbulb: Lightbulb,
airer: WindowCovering,
fan: {
service: Fanv2,
subName: '风干/烘干',
},
disinfection: {
service: Switch,
subName: '消毒',
},
});

this.services.lightbulb
.getCharacteristic(this.platform.Characteristic.On)
.onGet(this.getLightbulbOn.bind(this))
.onSet(this.setLightbulbOn.bind(this));

this.services.airer.getCharacteristic(this.platform.Characteristic.CurrentPosition).onGet(() => 50);

this.services.airer
.getCharacteristic(this.platform.Characteristic.TargetPosition)
.onGet(() => 50)
.onSet((value) => {
this.setResourceValue(Operation.Control, (value as number) > 50 ? '2' : '1');
});

this.services.airer
.getCharacteristic(this.platform.Characteristic.PositionState)
.onGet(() => this.getResourceValue(Operation.Control).then(({ value }) => this.positionStateMap[value]));

this.services.fan
.getCharacteristic(this.platform.Characteristic.Active)
.onGet(() =>
Promise.allSettled([this.getResourceValue(Operation.AirDry), this.getResourceValue(Operation.Dry)]).then(
([airDry, dry]) => {
return (
(airDry.status === 'fulfilled' && airDry.value.value === OpenClose.Open) ||
(dry.status === 'fulfilled' && dry.value.value === OpenClose.Open)
);
},
),
)
.onSet((value) => {
if (value) {
this.setResourceValue(Operation.AirDry, OpenClose.Open);
} else {
Promise.allSettled([
this.setResourceValue(Operation.AirDry, OpenClose.Close),
this.setResourceValue(Operation.Dry, OpenClose.Close),
]);
}
});

this.services.disinfection
.getCharacteristic(this.platform.Characteristic.On)
.onGet(() => this.getResourceValue(Operation.Disinfection).then(({ value }) => value === OpenClose.Open))
.onSet((value) => {
this.setResourceValue(Operation.Disinfection, value ? OpenClose.Open : OpenClose.Close);
});
}

async getLightbulbOn() {
return this.getResourceValue(Operation.Lightbulb).then(({ value }) => value === OpenClose.Open);
}

async setLightbulbOn(value) {
this.setResourceValue(Operation.Lightbulb, value ? OpenClose.Open : OpenClose.Close);
}

private positionStateMap = {
'1': this.platform.Characteristic.PositionState.DECREASING,
'2': this.platform.Characteristic.PositionState.INCREASING,
'0': this.platform.Characteristic.PositionState.STOPPED,
};
}
21 changes: 10 additions & 11 deletions src/accessories/lumi.airer.acn02.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,28 @@
import { BaseAccessory } from './base';

export class LumiAirerAcn02 extends BaseAccessory {
constructor(platform, accessory) {
super(platform, accessory);

this.init();
}

init() {
const { Lightbulb, WindowCovering } = this.platform.Service;
this.generateServices([Lightbulb, WindowCovering]);
this.generateServices({
lightbulb: Lightbulb,
airer: WindowCovering,
});

this.services[0]
this.services.lightbulb
.getCharacteristic(this.platform.Characteristic.On)
.onGet(this.getLightbulbOn.bind(this))
.onSet(this.setLightbulbOn.bind(this));

this.services[1].getCharacteristic(this.platform.Characteristic.CurrentPosition).onGet(this.getPosition.bind(this));
this.services.airer
.getCharacteristic(this.platform.Characteristic.CurrentPosition)
.onGet(this.getPosition.bind(this));

this.services[1]
this.services.airer
.getCharacteristic(this.platform.Characteristic.TargetPosition)
.onGet(this.getPosition.bind(this))
.onSet(this.setPosition.bind(this));

this.services[1]
this.services.airer
.getCharacteristic(this.platform.Characteristic.PositionState)
.onGet(this.getPositionState.bind(this));
}
Expand Down
89 changes: 89 additions & 0 deletions src/accessories/virtual.ir.tv.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { BaseAccessory } from './base';

export class VirtualIrTv extends BaseAccessory {
get did() {
return this.accessory.context.deviceInfo.did;
}

info?: Aqara.QueryIrInfoResponse;

keys: Aqara.QueryIrKeysResponse['keys'] = [];

get keyMap() {
const keyMap = new Map<string, Aqara.QueryIrKeysResponse['keys'][number]>();
this.keys.forEach((key) => {
keyMap.set(key.keyName, key);
});
return keyMap;
}

async init() {
const { Characteristic, Service } = this.platform;

await Promise.allSettled([
this.platform.aqaraApi.queryIrInfo(this.did),
this.platform.aqaraApi.queryIrKeys(this.did),
]).then(([info, keys]) => {
if (info.status === 'fulfilled') {
this.accessory
.getService(Service.AccessoryInformation)!
.setCharacteristic(Characteristic.Manufacturer, info.value.brandName);
this.info = info.value;
}
if (keys.status === 'fulfilled') {
this.keys = keys.value.keys;
}
});

this.generateServices({
tv: Service.Television,
});

this.services.tv.getCharacteristic(Characteristic.Active).onSet(this.setActive.bind(this));

this.services.tv.getCharacteristic(Characteristic.RemoteKey).onSet(this.setRemoteKey.bind(this));
}

setActive() {
this.writeIrClick('POWER');
}

setRemoteKey(value) {
const { RemoteKey } = this.platform.Characteristic;
const keyMap = {
[RemoteKey.REWIND]: 'REWIND',
[RemoteKey.FAST_FORWARD]: 'FAST_FORWARD',
[RemoteKey.NEXT_TRACK]: 'NEXT',
[RemoteKey.PREVIOUS_TRACK]: 'PREVIOUS',
[RemoteKey.ARROW_UP]: 'NAVIGATE_UP',
[RemoteKey.ARROW_DOWN]: 'NAVIGATE_DOWN',
[RemoteKey.ARROW_LEFT]: 'NAVIGATE_LEFT',
[RemoteKey.ARROW_RIGHT]: 'NAVIGATE_RIGHT',
[RemoteKey.SELECT]: 'OK',
[RemoteKey.BACK]: 'BACK',
[RemoteKey.EXIT]: 'BACK',
[RemoteKey.PLAY_PAUSE]: 'PAUSE',
[RemoteKey.INFORMATION]: 'DISPLAY',
};
const keyName = keyMap[value];
if (keyName) {
this.writeIrClick(keyName);
}
}

private writeIrClick(keyName: string) {
const keyInfo = this.keyMap.get(keyName);
if (!keyInfo) {
this.platform.log.error(`Key ${keyName} not found`);
return;
}
const { controllerId, keyId } = keyInfo;
const { did, brandId } = this.info!;
return this.platform.aqaraApi.request('write.ir.click', {
did,
brandId,
controllerId,
keyId,
});
}
}
17 changes: 14 additions & 3 deletions src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import fs from 'fs';
import { Logger } from 'homebridge/lib/logger';
import { BatchRequestManager } from 'batch-request-manager';
import { API_DOMAIN } from './constants';
import { inspect } from 'util';

export interface AqaraApiOption extends Aqara.AppConfig {
region: 'cn' | 'us' | 'kr' | 'ru' | 'eu' | 'sg';
Expand Down Expand Up @@ -113,8 +114,8 @@ export class AqaraApi {
};
}

private request<T>(intent: string, data: any) {
this.logger.info('Request:', intent, JSON.stringify(data));
request<T>(intent: string, data: any) {
this.logger.info('Request:', intent, inspect(data, true, Infinity));
return this.axios.post('/v3.0/open/api', {
intent,
data,
Expand Down Expand Up @@ -159,13 +160,14 @@ export class AqaraApi {
return this.request<Aqara.QueryDeviceInfoResponse>('query.device.info', params);
}

async getAllDevices() {
async getAllDevices(positionId?: string) {
try {
const pageSize = 100;
const result: Aqara.DeviceInfo[] = [];
const { data, totalCount } = await this.queryDeviceInfo({
pageNum: 1,
pageSize,
positionId,
});
result.push(...data);
if (totalCount > pageSize) {
Expand All @@ -174,6 +176,7 @@ export class AqaraApi {
const { data } = await this.queryDeviceInfo({
pageNum: i,
pageSize,
positionId,
});
result.push(...data);
}
Expand Down Expand Up @@ -231,4 +234,12 @@ export class AqaraApi {
setResourceValue(subjectId: string, resourceId: string, value: string) {
return this.setResourceBrm.request({ subjectId, resourceId, value });
}

queryIrInfo(did: string) {
return this.request<Aqara.QueryIrInfoResponse>('query.ir.info', { did });
}

queryIrKeys(did: string) {
return this.request<Aqara.QueryIrKeysResponse>('query.ir.keys', { did });
}
}
Loading

0 comments on commit b81860f

Please sign in to comment.