@switchbot/homebridge-switchbot 5.1.1 → 5.1.2

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.
@@ -529,7 +529,88 @@ export class GenericDevice extends DeviceBase {
529
529
  }
530
530
 
531
531
  // Specific device classes can extend GenericDevice for custom behavior.
532
- export class BotDevice extends GenericDevice {}
532
+ export class BotDevice extends GenericDevice {
533
+ private lastCommand?: { on: boolean, at: number }
534
+
535
+ createHAPAccessory(api: any): any {
536
+ const config = this.cfg.devices?.find((device: any) => (device.deviceId ?? device.id) === this.opts.id)
537
+ // Keep the old per-device type override for configurations made before v5.
538
+ const display = config?.botDisplay ?? config?.type ?? 'switch'
539
+ const pressMode = config?.mode === 'press' || config?.mode === 'multipress'
540
+ || (display === 'door' && config?.mode !== 'switch')
541
+
542
+ const isOn = async () => {
543
+ if (pressMode) {
544
+ return false
545
+ }
546
+ if (this.lastCommand && Date.now() - this.lastCommand.at < 15_000) {
547
+ return this.lastCommand.on
548
+ }
549
+ const state = await this.getState()
550
+ return !!(state && (state.on === true || state.state === 'on' || state.power === 'on'))
551
+ }
552
+
553
+ const setOn = async (on: boolean) => {
554
+ if (pressMode && !on) {
555
+ return
556
+ }
557
+ const command = pressMode ? 'press' : on ? 'turnOn' : 'turnOff'
558
+ const result = await this.setState({ command, parameter: 'default', commandType: 'command' })
559
+ if (result !== true && result?.success !== true) {
560
+ throw new Error(`SwitchBot Bot ${command} failed: ${result?.reason ?? 'unknown error'}`)
561
+ }
562
+ if (!pressMode) {
563
+ this.lastCommand = { on, at: Date.now() }
564
+ }
565
+ }
566
+
567
+ const control = (name: string, active: number | boolean, inactive: number | boolean, requestedOn: (value: any) => boolean) => ({
568
+ get: async () => (await isOn()) ? active : inactive,
569
+ set: async (value: any) => setOn(requestedOn(value)),
570
+ ...(pressMode ? { refreshAfterSet: [name] } : {}),
571
+ })
572
+ const position = () => ({
573
+ CurrentPosition: { get: async () => (await isOn()) ? 100 : 0 },
574
+ PositionState: { get: async () => 2 },
575
+ TargetPosition: control('TargetPosition', 100, 0, value => Number(value) > 0),
576
+ })
577
+ const active = (value: any) => Number(value) === 1
578
+ const presentations: Record<string, { type: string, characteristics: Record<string, any> }> = {
579
+ switch: { type: 'Switch', characteristics: { On: control('On', true, false, value => value === true) } },
580
+ outlet: { type: 'Outlet', characteristics: { On: control('On', true, false, value => value === true) } },
581
+ door: { type: 'Door', characteristics: position() },
582
+ window: { type: 'Window', characteristics: position() },
583
+ windowcovering: { type: 'WindowCovering', characteristics: position() },
584
+ garagedoor: { type: 'GarageDoorOpener', characteristics: {
585
+ CurrentDoorState: { get: async () => (await isOn()) ? 0 : 1 },
586
+ TargetDoorState: control('TargetDoorState', 0, 1, value => Number(value) === 0),
587
+ ObstructionDetected: { get: async () => false },
588
+ } },
589
+ lock: { type: 'LockMechanism', characteristics: {
590
+ LockCurrentState: { get: async () => (await isOn()) ? 0 : 1 },
591
+ LockTargetState: control('LockTargetState', 0, 1, value => Number(value) === 0),
592
+ } },
593
+ faucet: { type: 'Faucet', characteristics: { Active: control('Active', 1, 0, active) } },
594
+ fan: { type: 'Fanv2', characteristics: { Active: control('Active', 1, 0, active) } },
595
+ stateful: { type: 'StatefulProgrammableSwitch', characteristics: {
596
+ ProgrammableSwitchEvent: {},
597
+ ProgrammableSwitchOutputState: {
598
+ ...control('ProgrammableSwitchOutputState', 1, 0, active),
599
+ ...(pressMode ? { refreshAfterSet: ['ProgrammableSwitchOutputState'] } : {}),
600
+ afterSet: (value: any, service: any, hap: any) => {
601
+ if (!pressMode || active(value)) {
602
+ service.getCharacteristic(hap.Characteristic.ProgrammableSwitchEvent).sendEventNotification(0)
603
+ }
604
+ },
605
+ },
606
+ } },
607
+ }
608
+
609
+ return {
610
+ services: [presentations[display] ?? presentations.switch],
611
+ }
612
+ }
613
+ }
533
614
 
534
615
  export class CurtainDevice extends GenericDevice {
535
616
  private lastKnownPosition = 0
@@ -1364,34 +1445,69 @@ export class RollerShadeDevice extends CurtainDevice {}
1364
1445
 
1365
1446
  export class Hub2Device extends GenericDevice {}
1366
1447
 
1448
+ /** Default ppm at or above which HomeKit is told the CO2 level is abnormal. */
1449
+ const CO2_ABNORMAL_PPM_DEFAULT = 1000
1450
+
1367
1451
  export class MeterDevice extends GenericDevice {
1452
+ /**
1453
+ * The Meter Pro (CO2) reports carbon dioxide as well. `opts.type` is 'meter'
1454
+ * for every meter variant, so check the configured device type.
1455
+ */
1456
+ protected reportsCO2(): boolean {
1457
+ return /co2/i.test(String((this.opts as any)?.deviceType ?? ''))
1458
+ }
1459
+
1460
+ /** The ppm at which HomeKit reports carbon dioxide as detected. */
1461
+ protected co2AbnormalThreshold(): number {
1462
+ const value = Number((this.cfg as any)?.co2AbnormalThreshold)
1463
+ return Number.isFinite(value) && value > 0 ? value : CO2_ABNORMAL_PPM_DEFAULT
1464
+ }
1465
+
1368
1466
  createHAPAccessory(api: any) {
1369
- return {
1370
- services: [
1371
- {
1372
- type: 'TemperatureSensor',
1373
- characteristics: {
1374
- CurrentTemperature: {
1375
- get: async () => {
1376
- const s = await this.getState()
1377
- return typeof s.temperature === 'number' ? s.temperature : 0
1378
- },
1467
+ const services: any[] = [
1468
+ {
1469
+ type: 'TemperatureSensor',
1470
+ characteristics: {
1471
+ CurrentTemperature: {
1472
+ get: async () => {
1473
+ const s = await this.getState()
1474
+ return typeof s.temperature === 'number' ? s.temperature : 0
1379
1475
  },
1380
1476
  },
1381
1477
  },
1382
- {
1383
- type: 'HumiditySensor',
1384
- characteristics: {
1385
- CurrentRelativeHumidity: {
1386
- get: async () => {
1387
- const s = await this.getState()
1388
- return typeof s.humidity === 'number' ? s.humidity : 0
1389
- },
1478
+ },
1479
+ {
1480
+ type: 'HumiditySensor',
1481
+ characteristics: {
1482
+ CurrentRelativeHumidity: {
1483
+ get: async () => {
1484
+ const s = await this.getState()
1485
+ return typeof s.humidity === 'number' ? s.humidity : 0
1390
1486
  },
1391
1487
  },
1392
1488
  },
1393
- ],
1489
+ },
1490
+ ]
1491
+
1492
+ if (this.reportsCO2()) {
1493
+ const co2 = async (): Promise<number> => {
1494
+ const s = await this.getState()
1495
+ return typeof s.co2 === 'number' ? s.co2 : 0
1496
+ }
1497
+ services.push({
1498
+ type: 'CarbonDioxideSensor',
1499
+ characteristics: {
1500
+ CarbonDioxideLevel: {
1501
+ get: co2,
1502
+ },
1503
+ CarbonDioxideDetected: {
1504
+ get: async () => ((await co2()) >= this.co2AbnormalThreshold() ? 1 : 0),
1505
+ },
1506
+ },
1507
+ })
1394
1508
  }
1509
+
1510
+ return { services }
1395
1511
  }
1396
1512
  }
1397
1513
 
@@ -199,6 +199,11 @@
199
199
  <input type="number" id="matterBatchJitter" min="0" max="300" step="1" value="0" />
200
200
  <small>Random startup delay before the first batch to reduce synchronized spikes. Default: 0.</small>
201
201
  </div>
202
+ <div class="form-group">
203
+ <label for="co2AbnormalThreshold">CO2 Detected Threshold (ppm)</label>
204
+ <input type="number" id="co2AbnormalThreshold" min="400" max="5000" step="1" value="1000" />
205
+ <small>Meter Pro (CO2) only. At or above this level HomeKit reports carbon dioxide as detected. Default: 1000.</small>
206
+ </div>
202
207
  <button type="button" id="saveAdvancedSettingsBtn">Save Advanced Settings</button>
203
208
  <div id="advancedSettingsStatus" style="margin-top: 8px; font-size: 12px;"></div>
204
209
  </form>
@@ -29,6 +29,7 @@ async function loadAdvancedSettings(): Promise<void> {
29
29
  ;(document.getElementById('enableBLE') as HTMLInputElement).checked = config.enableBLE !== false
30
30
  ;(document.getElementById('blePollingEnabled') as HTMLInputElement).checked = config.blePollingEnabled !== false
31
31
  ;(document.getElementById('blePollIntervalMs') as HTMLInputElement).value = String(config.blePollIntervalMs ?? 600000)
32
+ ;(document.getElementById('co2AbnormalThreshold') as HTMLInputElement).value = String(config.co2AbnormalThreshold ?? 1000)
32
33
  } catch (e) {
33
34
  (document.getElementById('advancedSettingsStatus') as HTMLElement).textContent = 'Failed to load settings.'
34
35
  }
@@ -67,6 +68,7 @@ async function saveAdvancedSettings(): Promise<void> {
67
68
  config.enableBLE = (document.getElementById('enableBLE') as HTMLInputElement).checked
68
69
  config.blePollingEnabled = (document.getElementById('blePollingEnabled') as HTMLInputElement).checked
69
70
  config.blePollIntervalMs = Number((document.getElementById('blePollIntervalMs') as HTMLInputElement).value) || 600000
71
+ config.co2AbnormalThreshold = Number((document.getElementById('co2AbnormalThreshold') as HTMLInputElement).value) || 1000
70
72
 
71
73
  // Update config in memory and save to disk
72
74
  if (typeof homebridge.updatePluginConfig === 'function') {
@@ -0,0 +1,184 @@
1
+ import { Accessory, Characteristic, Service, uuid } from '@homebridge/hap-nodejs'
2
+ import { describe, expect, it, vi } from 'vitest'
3
+
4
+ import { BotDevice } from '../../src/devices/genericDevice.js'
5
+ import { SwitchBotHAPPlatform } from '../../src/SwitchBotHAPPlatform.js'
6
+
7
+ const id = 'bot-door-test'
8
+ const log = { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() }
9
+
10
+ function bot(config: Record<string, unknown>, result: unknown = { success: true }) {
11
+ const setDeviceState = vi.fn(async () => result)
12
+ const device = new BotDevice(
13
+ { id, type: 'Bot', name: 'Entrance', log, blePollingEnabled: false },
14
+ { log, devices: [{ deviceId: id, configDeviceType: 'Bot', ...config }], _client: { setDeviceState } } as any,
15
+ )
16
+ return { device, setDeviceState }
17
+ }
18
+
19
+ const displays = [
20
+ ['switch', 'Switch', 'On', true, false],
21
+ ['outlet', 'Outlet', 'On', true, false],
22
+ ['door', 'Door', 'TargetPosition', 100, 0],
23
+ ['window', 'Window', 'TargetPosition', 100, 0],
24
+ ['windowcovering', 'WindowCovering', 'TargetPosition', 100, 0],
25
+ ['garagedoor', 'GarageDoorOpener', 'TargetDoorState', 0, 1],
26
+ ['lock', 'LockMechanism', 'LockTargetState', 0, 1],
27
+ ['faucet', 'Faucet', 'Active', 1, 0],
28
+ ['fan', 'Fanv2', 'Active', 1, 0],
29
+ ['stateful', 'StatefulProgrammableSwitch', 'ProgrammableSwitchOutputState', 1, 0],
30
+ ] as const
31
+
32
+ describe('bot HomeKit presentation', () => {
33
+ it('keeps the default Switch presentation', () => {
34
+ expect(bot({}).device.createHAPAccessory({}).services[0].type).toBe('Switch')
35
+ })
36
+
37
+ it.each(displays)('restores %s as a HAP %s with Press Mode', async (display, serviceType, controlName, active, inactive) => {
38
+ const { device, setDeviceState } = bot({ botDisplay: display, mode: 'press' })
39
+ const descriptor = device.createHAPAccessory({}).services[0]
40
+ expect(descriptor.type).toBe(serviceType)
41
+ const required = new (Service as any)[serviceType]().characteristics
42
+ const provided = new Set(Object.keys(descriptor.characteristics).map(name => (Characteristic as any)[name]?.UUID))
43
+ for (const characteristic of required) {
44
+ expect(provided.has(characteristic.UUID)).toBe(true)
45
+ }
46
+ const control = descriptor.characteristics[controlName]
47
+ await control.set(active)
48
+ await control.set(inactive)
49
+ expect(await control.get()).toBe(inactive)
50
+ expect(setDeviceState).toHaveBeenCalledExactlyOnceWith(id, {
51
+ command: 'press',
52
+ parameter: 'default',
53
+ commandType: 'command',
54
+ })
55
+ })
56
+
57
+ it.each(displays)('registers %s and resets its HAP control after a press', async (display, serviceType, controlName, active, inactive) => {
58
+ const { device } = bot({ botDisplay: display, mode: 'press' })
59
+ const accessory = new Accessory('Entrance', uuid.generate(id))
60
+ accessory.addService(Service.Switch)
61
+ const api = { hap: { Service, Characteristic, uuid }, registerPlatformAccessories: vi.fn() }
62
+ const platform = Object.create(SwitchBotHAPPlatform.prototype) as any
63
+ platform.api = api
64
+ platform.log = log
65
+ platform.accessories = new Map([[uuid.generate(id), accessory]])
66
+ platform.hapPushTargets = new Map()
67
+ const created = { instance: device, createAccessory: () => device.createHAPAccessory(api) }
68
+
69
+ await platform.registerHAPAccessories([{ created, d: { id, name: 'Entrance' }, type: 'Bot' }])
70
+
71
+ const service = accessory.getService((Service as any)[serviceType])!
72
+ expect(service).toBeDefined()
73
+ if (serviceType !== 'Switch') {
74
+ expect(accessory.getService(Service.Switch)).toBeUndefined()
75
+ }
76
+ const characteristic = service.getCharacteristic((Characteristic as any)[controlName])
77
+ await characteristic.handleSetRequest(active)
78
+ await new Promise(resolve => setTimeout(resolve, 10))
79
+ expect(characteristic.value).toBe(inactive)
80
+ })
81
+
82
+ it('uses turnOn and turnOff for physical Switch Mode', async () => {
83
+ const { device, setDeviceState } = bot({ botDisplay: 'door', mode: 'switch' })
84
+ const target = device.createHAPAccessory({}).services[0].characteristics.TargetPosition
85
+ await target.set(100)
86
+ expect(await target.get()).toBe(100)
87
+ await target.set(0)
88
+ expect(await target.get()).toBe(0)
89
+ expect(setDeviceState.mock.calls.map(([, body]) => body.command)).toEqual(['turnOn', 'turnOff'])
90
+ })
91
+
92
+ it('reloads a Bot when its physical mode changes', async () => {
93
+ const platform = Object.create(SwitchBotHAPPlatform.prototype) as any
94
+ platform.config = { devices: [{ deviceId: id, configDeviceType: 'Bot', mode: 'switch' }] }
95
+ platform.lastConfigHash = platform.getConfigHash()
96
+ platform.config.devices[0].mode = 'press'
97
+ platform.log = log
98
+ platform.accessories = new Map()
99
+ platform.devices = []
100
+ platform.loadDevices = vi.fn(async () => {})
101
+
102
+ await platform.checkAndReloadDevices()
103
+
104
+ expect(platform.loadDevices).toHaveBeenCalledOnce()
105
+ })
106
+
107
+ it('emits each Stateful Programmable Switch press as an event', async () => {
108
+ const { device, setDeviceState } = bot({ botDisplay: 'stateful', mode: 'press' })
109
+ const accessory = new Accessory('Entrance', uuid.generate(id))
110
+ const api = { hap: { Service, Characteristic, uuid }, registerPlatformAccessories: vi.fn() }
111
+ const platform = Object.create(SwitchBotHAPPlatform.prototype) as any
112
+ platform.api = api
113
+ platform.log = log
114
+ platform.accessories = new Map([[uuid.generate(id), accessory]])
115
+ platform.hapPushTargets = new Map()
116
+ const created = { instance: device, createAccessory: () => device.createHAPAccessory(api) }
117
+ await platform.registerHAPAccessories([{ created, d: { id, name: 'Entrance' }, type: 'Bot' }])
118
+
119
+ const service = accessory.getService(Service.StatefulProgrammableSwitch)!
120
+ const event = service.getCharacteristic(Characteristic.ProgrammableSwitchEvent)
121
+ const notify = vi.spyOn(event, 'sendEventNotification')
122
+ const output = service.getCharacteristic(Characteristic.ProgrammableSwitchOutputState)
123
+ await output.handleSetRequest(1)
124
+ await new Promise(resolve => setTimeout(resolve, 10))
125
+ await output.handleSetRequest(1)
126
+ await new Promise(resolve => setTimeout(resolve, 10))
127
+ await output.handleSetRequest(0)
128
+ await new Promise(resolve => setTimeout(resolve, 10))
129
+
130
+ expect(notify).toHaveBeenCalledTimes(2)
131
+ expect(notify).toHaveBeenCalledWith(0)
132
+ expect(setDeviceState).toHaveBeenCalledTimes(2)
133
+ })
134
+
135
+ it('sends one press for Open and none for Close', async () => {
136
+ const { device, setDeviceState } = bot({ botDisplay: 'door' })
137
+ const chars = device.createHAPAccessory({}).services[0].characteristics
138
+ expect(await chars.TargetPosition.get()).toBe(0)
139
+ expect(await chars.CurrentPosition.get()).toBe(0)
140
+ expect(await chars.PositionState.get()).toBe(2)
141
+ await chars.TargetPosition.set(100)
142
+ await chars.TargetPosition.set(0)
143
+ expect(setDeviceState).toHaveBeenCalledExactlyOnceWith(id, {
144
+ command: 'press',
145
+ parameter: 'default',
146
+ commandType: 'command',
147
+ })
148
+ })
149
+
150
+ it('reports a failed press to HomeKit', async () => {
151
+ const { device } = bot({ type: 'door' }, { success: false, reason: 'offline' })
152
+ await expect(device.createHAPAccessory({}).services[0].characteristics.TargetPosition.set(100))
153
+ .rejects
154
+ .toThrow('offline')
155
+ })
156
+
157
+ it('replaces a cached Switch and resets TargetPosition after HAP commits the write', async () => {
158
+ const { device, setDeviceState } = bot({ botDisplay: 'door' })
159
+ const accessory = new Accessory('Entrance', uuid.generate(id))
160
+ accessory.addService(Service.Switch)
161
+ const api = {
162
+ hap: { Service, Characteristic, uuid },
163
+ registerPlatformAccessories: vi.fn(),
164
+ }
165
+ const platform = Object.create(SwitchBotHAPPlatform.prototype) as any
166
+ platform.api = api
167
+ platform.log = log
168
+ platform.accessories = new Map([[uuid.generate(id), accessory]])
169
+ platform.hapPushTargets = new Map()
170
+ const created = { instance: device, createAccessory: () => device.createHAPAccessory(api) }
171
+
172
+ await platform.registerHAPAccessories([{ created, d: { id, name: 'Entrance' }, type: 'Bot' }])
173
+
174
+ expect(accessory.getService(Service.Switch)).toBeUndefined()
175
+ const door = accessory.getService(Service.Door)!
176
+ expect(door.getCharacteristic(Characteristic.CurrentPosition)).toBeDefined()
177
+ expect(door.getCharacteristic(Characteristic.PositionState)).toBeDefined()
178
+ const target = door.getCharacteristic(Characteristic.TargetPosition)
179
+ await target.handleSetRequest(100)
180
+ await new Promise(resolve => setTimeout(resolve, 20))
181
+ expect(target.value).toBe(0)
182
+ expect(setDeviceState).toHaveBeenCalledTimes(1)
183
+ })
184
+ })
@@ -0,0 +1,96 @@
1
+ /* Copyright(C) 2021-2026, donavanbecker (https://github.com/donavanbecker). All rights reserved.
2
+ *
3
+ * meter-co2.spec.ts: the Meter Pro (CO2) exposes a CarbonDioxideSensor service
4
+ */
5
+
6
+ import { WoSensorTHProCO2 } from 'node-switchbot'
7
+ import { describe, expect, it, vi } from 'vitest'
8
+
9
+ import { createDevice } from '../../src/deviceFactory.js'
10
+ import { MeterDevice } from '../../src/devices/genericDevice.js'
11
+
12
+ const log: any = { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn(), success: vi.fn() }
13
+
14
+ function meter(deviceType: string | undefined, status: Record<string, unknown>, cfg: Record<string, unknown> = {}) {
15
+ const device = new MeterDevice({ id: 'B0E9FED044E3', type: 'meter', deviceType, name: 'Meter', log } as any, { log, ...cfg } as any)
16
+ vi.spyOn(device, 'getState').mockResolvedValue(status)
17
+ return device
18
+ }
19
+
20
+ function serviceTypes(device: MeterDevice) {
21
+ return device.createHAPAccessory(null).services.map((s: any) => s.type)
22
+ }
23
+
24
+ function read(device: MeterDevice, type: string, characteristic: string) {
25
+ const service = device.createHAPAccessory(null).services.find((s: any) => s.type === type)
26
+ return service.characteristics[characteristic].get()
27
+ }
28
+
29
+ const READING = { temperature: 22.7, humidity: 55, co2: 483 }
30
+
31
+ describe('meterDevice CO2 service', () => {
32
+ it.each(['MeterPro(CO2)', 'Meter Pro (CO2)'])('is added for %s', (deviceType) => {
33
+ expect(serviceTypes(meter(deviceType, READING))).toEqual(['TemperatureSensor', 'HumiditySensor', 'CarbonDioxideSensor'])
34
+ })
35
+
36
+ it.each(['Meter', 'Meter Plus', 'MeterPro', 'Outdoor Meter', undefined])('is not added for %s', (deviceType) => {
37
+ expect(serviceTypes(meter(deviceType, READING))).toEqual(['TemperatureSensor', 'HumiditySensor'])
38
+ })
39
+
40
+ it('reports the CO2 level in ppm', async () => {
41
+ await expect(read(meter('MeterPro(CO2)', READING), 'CarbonDioxideSensor', 'CarbonDioxideLevel')).resolves.toBe(483)
42
+ })
43
+
44
+ it('leaves temperature and humidity as they were', async () => {
45
+ const device = meter('MeterPro(CO2)', READING)
46
+
47
+ await expect(read(device, 'TemperatureSensor', 'CurrentTemperature')).resolves.toBe(22.7)
48
+ await expect(read(device, 'HumiditySensor', 'CurrentRelativeHumidity')).resolves.toBe(55)
49
+ })
50
+ })
51
+
52
+ describe('meterDevice CO2 detected threshold', () => {
53
+ const detected = (co2: number, cfg: Record<string, unknown> = {}) =>
54
+ read(meter('MeterPro(CO2)', { ...READING, co2 }, cfg), 'CarbonDioxideSensor', 'CarbonDioxideDetected')
55
+
56
+ it('defaults to 1000 ppm', async () => {
57
+ await expect(detected(999)).resolves.toBe(0)
58
+ await expect(detected(1000)).resolves.toBe(1)
59
+ })
60
+
61
+ it('uses co2AbnormalThreshold when set', async () => {
62
+ await expect(detected(799, { co2AbnormalThreshold: 800 })).resolves.toBe(0)
63
+ await expect(detected(800, { co2AbnormalThreshold: 800 })).resolves.toBe(1)
64
+ })
65
+
66
+ it.each([['zero', 0], ['negative', -1], ['text', 'high'], ['missing', undefined]])(
67
+ 'falls back to 1000 when the setting is %s',
68
+ async (_label, value) => {
69
+ await expect(detected(999, { co2AbnormalThreshold: value })).resolves.toBe(0)
70
+ await expect(detected(1000, { co2AbnormalThreshold: value })).resolves.toBe(1)
71
+ },
72
+ )
73
+ })
74
+
75
+ // End to end: the plugin's createDevice() with the real node-switchbot class,
76
+ // fed the status body a real Meter Pro (CO2) returned from the OpenAPI.
77
+ describe('a Meter Pro (CO2) end to end', () => {
78
+ it('reports CO2 to HomeKit through node-switchbot', async () => {
79
+ const apiBody = { version: 'V1.8', temperature: 22.7, battery: 100, humidity: 55, CO2: 483, deviceId: 'B0E9FED044E3', deviceType: 'MeterPro(CO2)', hubDeviceId: '000000000000' }
80
+ const switchBotDevice = new WoSensorTHProCO2(
81
+ { id: 'B0E9FED044E3', name: 'Meter Pro CO2 Monitor', deviceType: 'MeterPro(CO2)', connectionTypes: ['api'] } as any,
82
+ { apiClient: { getStatus: async () => apiBody } } as any,
83
+ )
84
+ const client = { init: async () => {}, getDevice: async () => switchBotDevice }
85
+
86
+ const created: any = await createDevice(
87
+ { id: 'B0E9FED044E3', type: 'meter', deviceType: 'MeterPro(CO2)', name: 'Meter Pro CO2 Monitor', log } as any,
88
+ { log, _client: client } as any,
89
+ false,
90
+ )
91
+ const service = created.createAccessory(null).services.find((s: any) => s.type === 'CarbonDioxideSensor')
92
+
93
+ await expect(service.characteristics.CarbonDioxideLevel.get()).resolves.toBe(483)
94
+ await expect(service.characteristics.CarbonDioxideDetected.get()).resolves.toBe(0)
95
+ })
96
+ })