homebridge-tsvesync 1.3.11 → 1.3.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/README.md +3 -0
- package/dist/accessories/air-quality-sensor.accessory.d.ts +4 -0
- package/dist/accessories/air-quality-sensor.accessory.d.ts.map +1 -1
- package/dist/accessories/air-quality-sensor.accessory.js +81 -12
- package/dist/accessories/air-quality-sensor.accessory.js.map +1 -1
- package/dist/accessories/base.accessory.d.ts.map +1 -1
- package/dist/accessories/base.accessory.js +1 -3
- package/dist/accessories/base.accessory.js.map +1 -1
- package/dist/accessories/light.accessory.d.ts.map +1 -1
- package/dist/accessories/light.accessory.js +27 -2
- package/dist/accessories/light.accessory.js.map +1 -1
- package/dist/types/device.types.d.ts +6 -0
- package/dist/types/device.types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/accessories/air-purifier-integration.test.ts +2 -2
- package/src/__tests__/accessories/air-quality-methods.test.ts +10 -12
- package/src/__tests__/accessories/air-quality-sensor.accessory.test.ts +231 -0
- package/src/__tests__/base.accessory.test.ts +2 -2
- package/src/__tests__/devices/light.test.ts +4 -0
- package/src/accessories/air-quality-sensor.accessory.ts +93 -15
- package/src/accessories/base.accessory.ts +1 -2
- package/src/accessories/light.accessory.ts +28 -2
- package/src/types/device.types.ts +3 -0
- package/src/__tests__/accessories/air-purifier-air-quality.test.ts +0 -483
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { API, Logger } from 'homebridge';
|
|
2
|
+
import { TSVESyncPlatform } from '../../platform';
|
|
3
|
+
import { VeSync } from 'tsvesync';
|
|
4
|
+
import { AirQualitySensorAccessory } from '../../accessories/air-quality-sensor.accessory';
|
|
5
|
+
import { createMockLogger, createMockVeSync } from '../utils/test-helpers';
|
|
6
|
+
|
|
7
|
+
interface MockAirQualityDevice {
|
|
8
|
+
deviceName: string;
|
|
9
|
+
deviceType: string;
|
|
10
|
+
cid: string;
|
|
11
|
+
uuid: string;
|
|
12
|
+
deviceStatus: string;
|
|
13
|
+
airQualityValue: number;
|
|
14
|
+
details: Record<string, any>;
|
|
15
|
+
getDetails: jest.Mock;
|
|
16
|
+
getNormalizedAirQuality?: jest.Mock;
|
|
17
|
+
airQualityLevel?: number;
|
|
18
|
+
airQualityLabel?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe('AirQualitySensorAccessory', () => {
|
|
22
|
+
let mockAPI: jest.Mocked<API>;
|
|
23
|
+
let platform: TSVESyncPlatform;
|
|
24
|
+
let logger: jest.Mocked<Logger>;
|
|
25
|
+
let api: jest.Mocked<VeSync>;
|
|
26
|
+
|
|
27
|
+
const AirQualityEnum = {
|
|
28
|
+
UNKNOWN: 0,
|
|
29
|
+
EXCELLENT: 1,
|
|
30
|
+
GOOD: 2,
|
|
31
|
+
FAIR: 3,
|
|
32
|
+
INFERIOR: 4,
|
|
33
|
+
POOR: 5,
|
|
34
|
+
} as const;
|
|
35
|
+
|
|
36
|
+
const ServiceEnum = {
|
|
37
|
+
AirQualitySensor: 'AirQualitySensor',
|
|
38
|
+
AccessoryInformation: 'AccessoryInformation',
|
|
39
|
+
} as const;
|
|
40
|
+
|
|
41
|
+
const createDevice = (overrides: Partial<MockAirQualityDevice> = {}): MockAirQualityDevice => ({
|
|
42
|
+
deviceName: overrides.deviceName || 'Dining Room AQ',
|
|
43
|
+
deviceType: overrides.deviceType || 'Core300S',
|
|
44
|
+
cid: overrides.cid || 'cid-123',
|
|
45
|
+
uuid: overrides.uuid || 'uuid-123',
|
|
46
|
+
deviceStatus: overrides.deviceStatus || 'on',
|
|
47
|
+
airQualityValue: overrides.airQualityValue ?? 18,
|
|
48
|
+
details: overrides.details || {
|
|
49
|
+
air_quality_value: overrides.airQualityValue ?? 18,
|
|
50
|
+
air_quality_level: overrides.airQualityLevel,
|
|
51
|
+
air_quality: overrides.airQualityLabel ?? 'good',
|
|
52
|
+
pm10: 22,
|
|
53
|
+
},
|
|
54
|
+
getDetails: overrides.getDetails || jest.fn().mockResolvedValue(true),
|
|
55
|
+
getNormalizedAirQuality: overrides.getNormalizedAirQuality,
|
|
56
|
+
airQualityLevel: overrides.airQualityLevel,
|
|
57
|
+
airQualityLabel: overrides.airQualityLabel,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const buildAccessory = (device: MockAirQualityDevice) => {
|
|
61
|
+
const airQualityService = {
|
|
62
|
+
getCharacteristic: jest.fn().mockImplementation((characteristic) => {
|
|
63
|
+
if (characteristic === AirQualityEnum) {
|
|
64
|
+
return {
|
|
65
|
+
setProps: jest.fn().mockReturnThis(),
|
|
66
|
+
onGet: jest.fn().mockReturnThis(),
|
|
67
|
+
updateValue: jest.fn().mockReturnThis(),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
onGet: jest.fn().mockReturnThis(),
|
|
72
|
+
updateValue: jest.fn().mockReturnThis(),
|
|
73
|
+
setProps: jest.fn().mockReturnThis(),
|
|
74
|
+
};
|
|
75
|
+
}),
|
|
76
|
+
setCharacteristic: jest.fn().mockReturnThis(),
|
|
77
|
+
updateCharacteristic: jest.fn().mockReturnThis(),
|
|
78
|
+
} as any;
|
|
79
|
+
|
|
80
|
+
const infoService = {
|
|
81
|
+
setCharacteristic: jest.fn().mockReturnThis(),
|
|
82
|
+
} as any;
|
|
83
|
+
|
|
84
|
+
const accessory = {
|
|
85
|
+
getService: jest.fn((service) => {
|
|
86
|
+
if (service === ServiceEnum.AccessoryInformation) return infoService;
|
|
87
|
+
if (service === ServiceEnum.AirQualitySensor) return null;
|
|
88
|
+
return null;
|
|
89
|
+
}),
|
|
90
|
+
addService: jest.fn((service) => {
|
|
91
|
+
if (service === ServiceEnum.AirQualitySensor) {
|
|
92
|
+
return airQualityService;
|
|
93
|
+
}
|
|
94
|
+
return airQualityService;
|
|
95
|
+
}),
|
|
96
|
+
} as any;
|
|
97
|
+
|
|
98
|
+
const accessoryInstance = new AirQualitySensorAccessory(platform, accessory, device as any);
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
accessoryInstance,
|
|
102
|
+
airQualityService,
|
|
103
|
+
infoService,
|
|
104
|
+
accessory,
|
|
105
|
+
};
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
beforeEach(() => {
|
|
109
|
+
logger = createMockLogger();
|
|
110
|
+
api = createMockVeSync();
|
|
111
|
+
|
|
112
|
+
mockAPI = {
|
|
113
|
+
hap: {
|
|
114
|
+
Characteristic: {
|
|
115
|
+
AirQuality: AirQualityEnum,
|
|
116
|
+
PM2_5Density: 'PM2_5Density',
|
|
117
|
+
PM10Density: 'PM10Density',
|
|
118
|
+
Manufacturer: 'Manufacturer',
|
|
119
|
+
Model: 'Model',
|
|
120
|
+
SerialNumber: 'SerialNumber',
|
|
121
|
+
Name: 'Name',
|
|
122
|
+
},
|
|
123
|
+
Service: ServiceEnum,
|
|
124
|
+
uuid: {
|
|
125
|
+
generate: jest.fn(),
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
platformAccessory: jest.fn(),
|
|
129
|
+
} as unknown as jest.Mocked<API>;
|
|
130
|
+
|
|
131
|
+
platform = new TSVESyncPlatform(logger, { debug: false, retry: { maxRetries: 3 } } as any, mockAPI);
|
|
132
|
+
(platform as any).api = api;
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('configures AirQuality characteristic to expose VeSync levels', () => {
|
|
136
|
+
const device = createDevice({
|
|
137
|
+
getNormalizedAirQuality: jest.fn().mockReturnValue({ level: 2, label: 'good' }),
|
|
138
|
+
airQualityLevel: 2,
|
|
139
|
+
airQualityLabel: 'good',
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const { airQualityService } = buildAccessory(device);
|
|
143
|
+
|
|
144
|
+
expect(airQualityService.getCharacteristic).toHaveBeenCalledWith(AirQualityEnum);
|
|
145
|
+
const setter = airQualityService.getCharacteristic.mock.results[0].value.setProps;
|
|
146
|
+
expect(setter).toHaveBeenCalledWith({
|
|
147
|
+
validValues: [
|
|
148
|
+
AirQualityEnum.UNKNOWN,
|
|
149
|
+
AirQualityEnum.EXCELLENT,
|
|
150
|
+
AirQualityEnum.GOOD,
|
|
151
|
+
AirQualityEnum.FAIR,
|
|
152
|
+
AirQualityEnum.INFERIOR,
|
|
153
|
+
],
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('returns normalized air quality levels when provided by the device', async () => {
|
|
158
|
+
const device = createDevice({
|
|
159
|
+
getNormalizedAirQuality: jest.fn().mockReturnValue({ level: 3, label: 'moderate' }),
|
|
160
|
+
airQualityValue: 42,
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const { accessoryInstance } = buildAccessory(device);
|
|
164
|
+
|
|
165
|
+
const value = await (accessoryInstance as any).getAirQuality();
|
|
166
|
+
expect(value).toBe(AirQualityEnum.FAIR);
|
|
167
|
+
expect(device.getNormalizedAirQuality).toHaveBeenCalled();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('falls back to direct level properties when normalized metadata is unavailable', async () => {
|
|
171
|
+
const device = createDevice({
|
|
172
|
+
getNormalizedAirQuality: undefined,
|
|
173
|
+
airQualityLevel: 4,
|
|
174
|
+
details: {
|
|
175
|
+
air_quality_level: 4,
|
|
176
|
+
air_quality_value: 80,
|
|
177
|
+
},
|
|
178
|
+
airQualityValue: 80,
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const { accessoryInstance } = buildAccessory(device);
|
|
182
|
+
|
|
183
|
+
const value = await (accessoryInstance as any).getAirQuality();
|
|
184
|
+
expect(value).toBe(AirQualityEnum.INFERIOR);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('falls back to PM2.5 heuristic when no level information is available', async () => {
|
|
188
|
+
const device = createDevice({
|
|
189
|
+
getNormalizedAirQuality: undefined,
|
|
190
|
+
airQualityLevel: undefined,
|
|
191
|
+
details: {
|
|
192
|
+
air_quality_value: 120,
|
|
193
|
+
},
|
|
194
|
+
airQualityValue: 120,
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const { accessoryInstance } = buildAccessory(device);
|
|
198
|
+
|
|
199
|
+
const value = await (accessoryInstance as any).getAirQuality();
|
|
200
|
+
expect(value).toBe(AirQualityEnum.INFERIOR);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('clamps PM2.5 density to HomeKit-supported range', async () => {
|
|
204
|
+
const device = createDevice({
|
|
205
|
+
airQualityValue: 1450,
|
|
206
|
+
details: {
|
|
207
|
+
air_quality_value: 1450,
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const { accessoryInstance } = buildAccessory(device);
|
|
212
|
+
|
|
213
|
+
const density = await (accessoryInstance as any).getPM25Density();
|
|
214
|
+
expect(density).toBe(1000);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('updates characteristics during refresh cycle', async () => {
|
|
218
|
+
const device = createDevice({
|
|
219
|
+
airQualityValue: 24,
|
|
220
|
+
getNormalizedAirQuality: jest.fn().mockReturnValue({ level: 2, label: 'good' }),
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
const { accessoryInstance, airQualityService } = buildAccessory(device);
|
|
224
|
+
|
|
225
|
+
await (accessoryInstance as any).updateCharacteristics();
|
|
226
|
+
|
|
227
|
+
expect(device.getDetails).toHaveBeenCalled();
|
|
228
|
+
expect(airQualityService.updateCharacteristic).toHaveBeenCalledWith(AirQualityEnum, AirQualityEnum.GOOD);
|
|
229
|
+
expect(airQualityService.updateCharacteristic).toHaveBeenCalledWith('PM2_5Density', 24);
|
|
230
|
+
});
|
|
231
|
+
});
|
|
@@ -299,7 +299,7 @@ describe('BaseAccessory', () => {
|
|
|
299
299
|
expect((accessory as any).convertAirQualityToHomeKit(30)).toBe(2); // GOOD
|
|
300
300
|
expect((accessory as any).convertAirQualityToHomeKit(50)).toBe(3); // FAIR
|
|
301
301
|
expect((accessory as any).convertAirQualityToHomeKit(100)).toBe(4); // INFERIOR
|
|
302
|
-
expect((accessory as any).convertAirQualityToHomeKit(200)).toBe(
|
|
302
|
+
expect((accessory as any).convertAirQualityToHomeKit(200)).toBe(4); // INFERIOR
|
|
303
303
|
});
|
|
304
304
|
|
|
305
305
|
it('should persist device state', async () => {
|
|
@@ -309,4 +309,4 @@ describe('BaseAccessory', () => {
|
|
|
309
309
|
expect(mockAccessory.context.device.details.testKey).toBe('testValue');
|
|
310
310
|
});
|
|
311
311
|
});
|
|
312
|
-
});
|
|
312
|
+
});
|
|
@@ -547,6 +547,7 @@ describe('Light Device Tests', () => {
|
|
|
547
547
|
|
|
548
548
|
await onCharacteristic._onSet(true);
|
|
549
549
|
|
|
550
|
+
expect(dimmer.turnOn).toHaveBeenCalled();
|
|
550
551
|
expect(dimmer.setBrightness).toHaveBeenCalledWith(37);
|
|
551
552
|
});
|
|
552
553
|
|
|
@@ -556,10 +557,13 @@ describe('Light Device Tests', () => {
|
|
|
556
557
|
|
|
557
558
|
await onCharacteristic._onSet(true);
|
|
558
559
|
await onCharacteristic._onSet(false);
|
|
560
|
+
expect(dimmer.turnOn).toHaveBeenCalledTimes(1);
|
|
561
|
+
expect(dimmer.turnOff).toHaveBeenCalledTimes(1);
|
|
559
562
|
dimmer.setBrightness.mockClear();
|
|
560
563
|
|
|
561
564
|
await onCharacteristic._onSet(true);
|
|
562
565
|
|
|
566
|
+
expect(dimmer.turnOn).toHaveBeenCalledTimes(2);
|
|
563
567
|
expect(dimmer.setBrightness).toHaveBeenCalledWith(45);
|
|
564
568
|
});
|
|
565
569
|
|
|
@@ -43,7 +43,6 @@ export class AirQualitySensorAccessory extends BaseAccessory {
|
|
|
43
43
|
this.platform.Characteristic.AirQuality.GOOD,
|
|
44
44
|
this.platform.Characteristic.AirQuality.FAIR,
|
|
45
45
|
this.platform.Characteristic.AirQuality.INFERIOR,
|
|
46
|
-
this.platform.Characteristic.AirQuality.POOR,
|
|
47
46
|
],
|
|
48
47
|
})
|
|
49
48
|
.onGet(this.getAirQuality.bind(this));
|
|
@@ -91,12 +90,15 @@ export class AirQualitySensorAccessory extends BaseAccessory {
|
|
|
91
90
|
*/
|
|
92
91
|
async getAirQuality(): Promise<CharacteristicValue> {
|
|
93
92
|
try {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
93
|
+
const level = this.readNormalizedAirQualityLevel();
|
|
94
|
+
if (level >= 1) {
|
|
95
|
+
const hkValue = this.mapToHomeKitAirQuality(level);
|
|
96
|
+
this.platform.log.debug(`${this.device.deviceName} AQ: Normalized air quality level = ${level}`);
|
|
97
|
+
return hkValue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const pm25 = this.getDevicePm25();
|
|
101
|
+
this.platform.log.debug(`${this.device.deviceName} AQ: Falling back to PM2.5 heuristic (${pm25})`);
|
|
100
102
|
return this.convertPM25ToAirQuality(pm25);
|
|
101
103
|
} catch (error) {
|
|
102
104
|
this.platform.log.error(`${this.device.deviceName} AQ: Failed to get air quality:`, error);
|
|
@@ -109,9 +111,7 @@ export class AirQualitySensorAccessory extends BaseAccessory {
|
|
|
109
111
|
*/
|
|
110
112
|
async getPM25Density(): Promise<CharacteristicValue> {
|
|
111
113
|
try {
|
|
112
|
-
|
|
113
|
-
const extendedDevice = this.parentDevice as any;
|
|
114
|
-
const pm25 = extendedDevice.airQualityValue || 0;
|
|
114
|
+
const pm25 = this.getDevicePm25();
|
|
115
115
|
|
|
116
116
|
// HomeKit expects PM2.5 in μg/m³, range 0-1000
|
|
117
117
|
return Math.min(1000, Math.max(0, pm25));
|
|
@@ -148,11 +148,89 @@ export class AirQualitySensorAccessory extends BaseAccessory {
|
|
|
148
148
|
return this.platform.Characteristic.AirQuality.GOOD; // Moderate (12.1-35.4 μg/m³)
|
|
149
149
|
} else if (pm25 <= 55) {
|
|
150
150
|
return this.platform.Characteristic.AirQuality.FAIR; // Unhealthy for Sensitive (35.5-55.4 μg/m³)
|
|
151
|
-
} else if (pm25 <= 150) {
|
|
152
|
-
return this.platform.Characteristic.AirQuality.INFERIOR; // Unhealthy (55.5-150.4 μg/m³)
|
|
153
|
-
} else {
|
|
154
|
-
return this.platform.Characteristic.AirQuality.POOR; // Very Unhealthy (>150.5 μg/m³)
|
|
155
151
|
}
|
|
152
|
+
return this.platform.Characteristic.AirQuality.INFERIOR;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private mapToHomeKitAirQuality(level: number): CharacteristicValue {
|
|
156
|
+
switch (level) {
|
|
157
|
+
case 1:
|
|
158
|
+
return this.platform.Characteristic.AirQuality.EXCELLENT;
|
|
159
|
+
case 2:
|
|
160
|
+
return this.platform.Characteristic.AirQuality.GOOD;
|
|
161
|
+
case 3:
|
|
162
|
+
return this.platform.Characteristic.AirQuality.FAIR;
|
|
163
|
+
case 4:
|
|
164
|
+
return this.platform.Characteristic.AirQuality.INFERIOR;
|
|
165
|
+
default:
|
|
166
|
+
return this.platform.Characteristic.AirQuality.UNKNOWN;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private readNormalizedAirQualityLevel(): number {
|
|
171
|
+
const extendedDevice = this.parentDevice as any;
|
|
172
|
+
|
|
173
|
+
if (typeof extendedDevice.getNormalizedAirQuality === 'function') {
|
|
174
|
+
try {
|
|
175
|
+
const normalized = extendedDevice.getNormalizedAirQuality();
|
|
176
|
+
if (normalized && typeof normalized.level === 'number' && normalized.level >= 1) {
|
|
177
|
+
return normalized.level;
|
|
178
|
+
}
|
|
179
|
+
} catch (error) {
|
|
180
|
+
this.platform.log.debug(`${this.device.deviceName} AQ: getNormalizedAirQuality failed`, error);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const directLevel = extendedDevice.airQualityLevel ?? extendedDevice.details?.air_quality_level;
|
|
185
|
+
if (typeof directLevel === 'number' && directLevel >= 1) {
|
|
186
|
+
return directLevel;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const normalized = this.normalizeAirQuality(
|
|
190
|
+
extendedDevice.airQuality ?? extendedDevice.details?.air_quality ?? extendedDevice.airQualityLabel
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
return normalized.level;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private getDevicePm25(): number {
|
|
197
|
+
const extendedDevice = this.parentDevice as any;
|
|
198
|
+
const pm25 = extendedDevice.airQualityValue ?? extendedDevice.details?.air_quality_value ?? extendedDevice.pm25 ?? 0;
|
|
199
|
+
return Number.isFinite(pm25) ? pm25 : 0;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
private normalizeAirQuality(value: unknown): { level: number; label: string } {
|
|
203
|
+
const stringMap: Record<string, number> = {
|
|
204
|
+
'excellent': 1,
|
|
205
|
+
'very good': 1,
|
|
206
|
+
'good': 2,
|
|
207
|
+
'moderate': 3,
|
|
208
|
+
'fair': 3,
|
|
209
|
+
'inferior': 4,
|
|
210
|
+
'poor': 4,
|
|
211
|
+
'bad': 4,
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
if (typeof value === 'number') {
|
|
215
|
+
const level = Number.isFinite(value) ? Math.trunc(value) : -1;
|
|
216
|
+
if (level >= 1 && level <= 4) {
|
|
217
|
+
return {
|
|
218
|
+
level,
|
|
219
|
+
label: level === 1 ? 'excellent' : level === 2 ? 'good' : level === 3 ? 'moderate' : 'poor',
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
} else if (typeof value === 'string') {
|
|
223
|
+
const normalized = value.trim().toLowerCase();
|
|
224
|
+
const level = stringMap[normalized];
|
|
225
|
+
if (level) {
|
|
226
|
+
return {
|
|
227
|
+
level,
|
|
228
|
+
label: level === 1 ? 'excellent' : level === 2 ? 'good' : level === 3 ? 'moderate' : 'poor',
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return { level: -1, label: 'unknown' };
|
|
156
234
|
}
|
|
157
235
|
|
|
158
236
|
/**
|
|
@@ -234,4 +312,4 @@ export class AirQualitySensorAccessory extends BaseAccessory {
|
|
|
234
312
|
identify(): void {
|
|
235
313
|
this.platform.log.info(`${this.device.deviceName} AQ Sensor: Identify!`);
|
|
236
314
|
}
|
|
237
|
-
}
|
|
315
|
+
}
|
|
@@ -226,8 +226,7 @@ export abstract class BaseAccessory {
|
|
|
226
226
|
if (pm25 <= 12) return 1; // EXCELLENT
|
|
227
227
|
if (pm25 <= 35) return 2; // GOOD
|
|
228
228
|
if (pm25 <= 55) return 3; // FAIR
|
|
229
|
-
|
|
230
|
-
return 5; // POOR
|
|
229
|
+
return 4; // INFERIOR (VeSync exposes four levels)
|
|
231
230
|
}
|
|
232
231
|
|
|
233
232
|
/**
|
|
@@ -278,8 +278,34 @@ export class LightAccessory extends BaseAccessory {
|
|
|
278
278
|
const isOn = value as boolean;
|
|
279
279
|
|
|
280
280
|
if (this.isDimmerDevice) {
|
|
281
|
-
const
|
|
282
|
-
|
|
281
|
+
const dimmer = this.device as VeSyncDimmerSwitch;
|
|
282
|
+
if (isOn) {
|
|
283
|
+
const toggled = await dimmer.turnOn();
|
|
284
|
+
if (!toggled) {
|
|
285
|
+
throw new Error('Failed to turn on device');
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const targetBrightness = this.resolveDimmerOnBrightness();
|
|
289
|
+
if (targetBrightness > 0) {
|
|
290
|
+
const brightnessSet = await dimmer.setBrightness(targetBrightness);
|
|
291
|
+
if (!brightnessSet) {
|
|
292
|
+
throw new Error(`Failed to restore brightness to ${targetBrightness}`);
|
|
293
|
+
}
|
|
294
|
+
this.lastKnownDimmerBrightness = targetBrightness;
|
|
295
|
+
this.updateCharacteristicValue(this.platform.Characteristic.Brightness, targetBrightness);
|
|
296
|
+
await this.persistDeviceState('brightness', targetBrightness);
|
|
297
|
+
}
|
|
298
|
+
} else {
|
|
299
|
+
const toggled = await dimmer.turnOff();
|
|
300
|
+
if (!toggled) {
|
|
301
|
+
throw new Error('Failed to turn off device');
|
|
302
|
+
}
|
|
303
|
+
this.updateCharacteristicValue(this.platform.Characteristic.Brightness, 0);
|
|
304
|
+
await this.persistDeviceState('brightness', 0);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
await this.persistDeviceState('deviceStatus', isOn ? 'on' : 'off');
|
|
308
|
+
await this.refreshDimmerDetails();
|
|
283
309
|
return;
|
|
284
310
|
}
|
|
285
311
|
|
|
@@ -67,10 +67,13 @@ export interface VeSyncDeviceWithHumidity extends VeSyncDeviceWithPower {
|
|
|
67
67
|
|
|
68
68
|
export interface VeSyncDeviceWithAirQuality extends VeSyncDeviceWithPower {
|
|
69
69
|
airQuality: number;
|
|
70
|
+
airQualityLevel?: number;
|
|
71
|
+
airQualityLabel?: string;
|
|
70
72
|
pm25?: number;
|
|
71
73
|
filterLife: number;
|
|
72
74
|
mode: string;
|
|
73
75
|
setMode(mode: string): Promise<boolean>;
|
|
76
|
+
getNormalizedAirQuality?(): { level: number; label: string };
|
|
74
77
|
}
|
|
75
78
|
|
|
76
79
|
export interface VeSyncDeviceWithControls extends VeSyncDeviceWithPower {
|