homebridge-tuya-plus 3.9.0 → 3.11.0
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/config.schema.json
CHANGED
|
@@ -82,6 +82,10 @@
|
|
|
82
82
|
"title": "Garage Door",
|
|
83
83
|
"enum": ["GarageDoor"]
|
|
84
84
|
},
|
|
85
|
+
{
|
|
86
|
+
"title": "Simple Garage Door (Open/Stop/Close)",
|
|
87
|
+
"enum": ["SimpleGarageDoor"]
|
|
88
|
+
},
|
|
85
89
|
{
|
|
86
90
|
"title": "Simple Blinds",
|
|
87
91
|
"enum": ["SimpleBlinds"]
|
|
@@ -493,6 +497,27 @@
|
|
|
493
497
|
"functionBody": "return model.devices && model.devices[arrayIndices] && ['GarageDoor'].includes(model.devices[arrayIndices].type);"
|
|
494
498
|
}
|
|
495
499
|
},
|
|
500
|
+
"dpOpen": {
|
|
501
|
+
"type": "integer",
|
|
502
|
+
"placeholder": "1",
|
|
503
|
+
"condition": {
|
|
504
|
+
"functionBody": "return model.devices && model.devices[arrayIndices] && ['SimpleGarageDoor'].includes(model.devices[arrayIndices].type);"
|
|
505
|
+
}
|
|
506
|
+
},
|
|
507
|
+
"dpStop": {
|
|
508
|
+
"type": "integer",
|
|
509
|
+
"placeholder": "2",
|
|
510
|
+
"condition": {
|
|
511
|
+
"functionBody": "return model.devices && model.devices[arrayIndices] && ['SimpleGarageDoor'].includes(model.devices[arrayIndices].type);"
|
|
512
|
+
}
|
|
513
|
+
},
|
|
514
|
+
"dpClose": {
|
|
515
|
+
"type": "integer",
|
|
516
|
+
"placeholder": "3",
|
|
517
|
+
"condition": {
|
|
518
|
+
"functionBody": "return model.devices && model.devices[arrayIndices] && ['SimpleGarageDoor'].includes(model.devices[arrayIndices].type);"
|
|
519
|
+
}
|
|
520
|
+
},
|
|
496
521
|
"flipState": {
|
|
497
522
|
"type": "boolean",
|
|
498
523
|
"condition": {
|
package/index.js
CHANGED
|
@@ -13,6 +13,7 @@ const AirPurifierAccessory = require('./lib/AirPurifierAccessory');
|
|
|
13
13
|
const DehumidifierAccessory = require('./lib/DehumidifierAccessory');
|
|
14
14
|
const ConvectorAccessory = require('./lib/ConvectorAccessory');
|
|
15
15
|
const GarageDoorAccessory = require('./lib/GarageDoorAccessory');
|
|
16
|
+
const SimpleGarageDoorAccessory = require('./lib/SimpleGarageDoorAccessory');
|
|
16
17
|
const SimpleDimmerAccessory = require('./lib/SimpleDimmerAccessory');
|
|
17
18
|
const SimpleDimmer2Accessory = require('./lib/SimpleDimmer2Accessory');
|
|
18
19
|
const SimpleBlindsAccessory = require('./lib/SimpleBlindsAccessory');
|
|
@@ -47,6 +48,7 @@ const CLASS_DEF = {
|
|
|
47
48
|
dehumidifier: DehumidifierAccessory,
|
|
48
49
|
convector: ConvectorAccessory,
|
|
49
50
|
garagedoor: GarageDoorAccessory,
|
|
51
|
+
simplegaragedoor: SimpleGarageDoorAccessory,
|
|
50
52
|
simpledimmer: SimpleDimmerAccessory,
|
|
51
53
|
simpledimmer2: SimpleDimmer2Accessory,
|
|
52
54
|
simpleblinds: SimpleBlindsAccessory,
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
const BaseAccessory = require('./BaseAccessory');
|
|
2
2
|
|
|
3
|
-
const STATE_OTHER = 9;
|
|
4
|
-
|
|
5
3
|
class AirConditionerAccessory extends BaseAccessory {
|
|
6
4
|
static getCategory(Categories) {
|
|
7
5
|
return Categories.AIR_CONDITIONER;
|
|
@@ -28,8 +26,6 @@ class AirConditionerAccessory extends BaseAccessory {
|
|
|
28
26
|
else throw new Error('The cmdAuto doesn\'t appear to be valid: ' + this.device.context.cmdAuto);
|
|
29
27
|
}
|
|
30
28
|
|
|
31
|
-
this.device.context.noAuto = true;
|
|
32
|
-
|
|
33
29
|
if (!this.device.context.noRotationSpeed) {
|
|
34
30
|
const fanSpeedSteps = (this.device.context.fanSpeedSteps && isFinite(this.device.context.fanSpeedSteps) && this.device.context.fanSpeedSteps > 0 && this.device.context.fanSpeedSteps < 100) ? this.device.context.fanSpeedSteps : 100;
|
|
35
31
|
this._rotationSteps = [0];
|
|
@@ -65,7 +61,7 @@ class AirConditionerAccessory extends BaseAccessory {
|
|
|
65
61
|
this.dpSwingMode = this._getCustomDP(this.device.context.dpSwingMode) || '104';
|
|
66
62
|
|
|
67
63
|
const characteristicActive = service.getCharacteristic(Characteristic.Active)
|
|
68
|
-
.updateValue(this._getActive(this.dpActive))
|
|
64
|
+
.updateValue(this._getActive(dps[this.dpActive]))
|
|
69
65
|
.onGet(() => this.getActive())
|
|
70
66
|
.onSet(value => this.setActive(value));
|
|
71
67
|
|
|
@@ -73,13 +69,13 @@ class AirConditionerAccessory extends BaseAccessory {
|
|
|
73
69
|
.updateValue(this._getCurrentHeaterCoolerState(dps))
|
|
74
70
|
.onGet(() => this.getCurrentHeaterCoolerState());
|
|
75
71
|
|
|
76
|
-
const _validTargetHeaterCoolerStateValues = [
|
|
77
|
-
if (!this.device.context.
|
|
78
|
-
if (!this.device.context.noHeat) _validTargetHeaterCoolerStateValues.
|
|
79
|
-
if (!this.device.context.
|
|
72
|
+
const _validTargetHeaterCoolerStateValues = [];
|
|
73
|
+
if (!this.device.context.noAuto) _validTargetHeaterCoolerStateValues.push(Characteristic.TargetHeaterCoolerState.AUTO);
|
|
74
|
+
if (!this.device.context.noHeat) _validTargetHeaterCoolerStateValues.push(Characteristic.TargetHeaterCoolerState.HEAT);
|
|
75
|
+
if (!this.device.context.noCool) _validTargetHeaterCoolerStateValues.push(Characteristic.TargetHeaterCoolerState.COOL);
|
|
80
76
|
|
|
81
77
|
const characteristicTargetHeaterCoolerState = service.getCharacteristic(Characteristic.TargetHeaterCoolerState)
|
|
82
|
-
.setProps({
|
|
78
|
+
.setProps({ validValues: _validTargetHeaterCoolerStateValues })
|
|
83
79
|
.updateValue(this._getTargetHeaterCoolerState(dps[this.dpMode]))
|
|
84
80
|
.onGet(() => this.getTargetHeaterCoolerState())
|
|
85
81
|
.onSet(value => this.setTargetHeaterCoolerState(value));
|
|
@@ -112,7 +108,7 @@ class AirConditionerAccessory extends BaseAccessory {
|
|
|
112
108
|
maxValue: this.device.context.maxTemperature || 35,
|
|
113
109
|
minStep: this.device.context.minTemperatureSteps || 1
|
|
114
110
|
})
|
|
115
|
-
.updateValue(this.dpThreshold)
|
|
111
|
+
.updateValue(dps[this.dpThreshold])
|
|
116
112
|
.onGet(() => this.getStateAsync(this.dpThreshold))
|
|
117
113
|
.onSet(value => this.setTargetThresholdTemperature('cool', value));
|
|
118
114
|
} else this._removeCharacteristic(service, Characteristic.CoolingThresholdTemperature);
|
|
@@ -256,17 +252,20 @@ class AirConditionerAccessory extends BaseAccessory {
|
|
|
256
252
|
const {Characteristic} = this.hap;
|
|
257
253
|
switch (dp) {
|
|
258
254
|
case this.cmdCool:
|
|
259
|
-
if (this.device.context.noCool) return
|
|
260
|
-
|
|
255
|
+
if (!this.device.context.noCool) return Characteristic.TargetHeaterCoolerState.COOL;
|
|
256
|
+
break;
|
|
261
257
|
case this.cmdHeat:
|
|
262
|
-
if (this.device.context.noHeat) return
|
|
263
|
-
|
|
258
|
+
if (!this.device.context.noHeat) return Characteristic.TargetHeaterCoolerState.HEAT;
|
|
259
|
+
break;
|
|
264
260
|
case this.cmdAuto:
|
|
265
|
-
if (this.device.context.noAuto) return
|
|
266
|
-
|
|
267
|
-
default:
|
|
268
|
-
return STATE_OTHER;
|
|
261
|
+
if (!this.device.context.noAuto) return Characteristic.TargetHeaterCoolerState.AUTO;
|
|
262
|
+
break;
|
|
269
263
|
}
|
|
264
|
+
// Fall back to the first allowed mode rather than publishing a value
|
|
265
|
+
// that isn't in validValues — HomeKit would reject it otherwise.
|
|
266
|
+
if (!this.device.context.noCool) return Characteristic.TargetHeaterCoolerState.COOL;
|
|
267
|
+
if (!this.device.context.noHeat) return Characteristic.TargetHeaterCoolerState.HEAT;
|
|
268
|
+
return Characteristic.TargetHeaterCoolerState.AUTO;
|
|
270
269
|
}
|
|
271
270
|
|
|
272
271
|
setTargetHeaterCoolerState(value) {
|
|
@@ -274,13 +273,13 @@ class AirConditionerAccessory extends BaseAccessory {
|
|
|
274
273
|
switch (value) {
|
|
275
274
|
case Characteristic.TargetHeaterCoolerState.COOL:
|
|
276
275
|
if (this.device.context.noCool) return;
|
|
277
|
-
return this.
|
|
276
|
+
return this.setMultiStateLegacyAsync({[this.dpActive]: true, [this.dpMode]: this.cmdCool});
|
|
278
277
|
case Characteristic.TargetHeaterCoolerState.HEAT:
|
|
279
278
|
if (this.device.context.noHeat) return;
|
|
280
|
-
return this.
|
|
279
|
+
return this.setMultiStateLegacyAsync({[this.dpActive]: true, [this.dpMode]: this.cmdHeat});
|
|
281
280
|
case Characteristic.TargetHeaterCoolerState.AUTO:
|
|
282
281
|
if (this.device.context.noAuto) return;
|
|
283
|
-
return this.
|
|
282
|
+
return this.setMultiStateLegacyAsync({[this.dpActive]: true, [this.dpMode]: this.cmdAuto});
|
|
284
283
|
}
|
|
285
284
|
}
|
|
286
285
|
|
|
@@ -305,7 +304,7 @@ class AirConditionerAccessory extends BaseAccessory {
|
|
|
305
304
|
}
|
|
306
305
|
|
|
307
306
|
async setTargetThresholdTemperature(mode, value) {
|
|
308
|
-
await this.
|
|
307
|
+
await this.setMultiStateLegacyAsync({[this.dpActive]: true, [this.dpThreshold]: value});
|
|
309
308
|
if (mode === 'cool' && !this.device.context.noHeat && this.characteristicHeatingThresholdTemperature) {
|
|
310
309
|
this.characteristicHeatingThresholdTemperature.updateValue(value);
|
|
311
310
|
} else if (mode === 'heat' && !this.device.context.noCool && this.characteristicCoolingThresholdTemperature) {
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
const BaseAccessory = require('./BaseAccessory');
|
|
2
|
+
|
|
3
|
+
// Window during which CurrentDoorState lags TargetDoorState so HomeKit's
|
|
4
|
+
// "Opening..."/"Closing..." caption is visible after a toggle. The device
|
|
5
|
+
// has no position feedback, so this is purely cosmetic.
|
|
6
|
+
const CURRENT_STATE_DELAY_MS = 1000;
|
|
7
|
+
|
|
8
|
+
class SimpleGarageDoorAccessory extends BaseAccessory {
|
|
9
|
+
static getCategory(Categories) {
|
|
10
|
+
return Categories.GARAGE_DOOR_OPENER;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
constructor(...props) {
|
|
14
|
+
super(...props);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
_registerPlatformAccessory() {
|
|
18
|
+
const {Service} = this.hap;
|
|
19
|
+
|
|
20
|
+
this.accessory.addService(Service.GarageDoorOpener, this.device.context.name);
|
|
21
|
+
|
|
22
|
+
super._registerPlatformAccessory();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
_registerCharacteristics() {
|
|
26
|
+
const {Service, Characteristic} = this.hap;
|
|
27
|
+
const service = this.accessory.getService(Service.GarageDoorOpener);
|
|
28
|
+
this._checkServiceName(service, this.device.context.name);
|
|
29
|
+
|
|
30
|
+
this.dpOpen = this._getCustomDP(this.device.context.dpOpen) || '1';
|
|
31
|
+
this.dpStop = this._getCustomDP(this.device.context.dpStop) || '2';
|
|
32
|
+
this.dpClose = this._getCustomDP(this.device.context.dpClose) || '3';
|
|
33
|
+
|
|
34
|
+
// The device only exposes momentary action DPs, so the target state is
|
|
35
|
+
// tracked locally and persisted via the homebridge accessory context.
|
|
36
|
+
if (this.accessory.context.cachedTargetDoorState !== Characteristic.TargetDoorState.OPEN &&
|
|
37
|
+
this.accessory.context.cachedTargetDoorState !== Characteristic.TargetDoorState.CLOSED) {
|
|
38
|
+
this.accessory.context.cachedTargetDoorState = Characteristic.TargetDoorState.OPEN;
|
|
39
|
+
}
|
|
40
|
+
const initialTarget = this.accessory.context.cachedTargetDoorState;
|
|
41
|
+
this.currentDoorState = initialTarget === Characteristic.TargetDoorState.OPEN
|
|
42
|
+
? Characteristic.CurrentDoorState.OPEN
|
|
43
|
+
: Characteristic.CurrentDoorState.CLOSED;
|
|
44
|
+
|
|
45
|
+
this.characteristicTargetDoorState = service.getCharacteristic(Characteristic.TargetDoorState)
|
|
46
|
+
.updateValue(initialTarget)
|
|
47
|
+
.onGet(() => this.accessory.context.cachedTargetDoorState)
|
|
48
|
+
.onSet(value => this.setTargetDoorState(value));
|
|
49
|
+
|
|
50
|
+
this.characteristicCurrentDoorState = service.getCharacteristic(Characteristic.CurrentDoorState)
|
|
51
|
+
.updateValue(this.currentDoorState)
|
|
52
|
+
.onGet(() => this.currentDoorState);
|
|
53
|
+
|
|
54
|
+
service.getCharacteristic(Characteristic.ObstructionDetected)
|
|
55
|
+
.updateValue(false)
|
|
56
|
+
.onGet(() => false);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
setTargetDoorState(value) {
|
|
60
|
+
const {Characteristic} = this.hap;
|
|
61
|
+
|
|
62
|
+
this.accessory.context.cachedTargetDoorState = value;
|
|
63
|
+
|
|
64
|
+
// Send stop first so reversing direction mid-motion works; if the gate
|
|
65
|
+
// is already idle, the stop is a no-op on the device side.
|
|
66
|
+
this.setMultiStateLegacyAsync({[this.dpStop]: true});
|
|
67
|
+
if (value === Characteristic.TargetDoorState.OPEN) {
|
|
68
|
+
this.setMultiStateLegacyAsync({[this.dpOpen]: true});
|
|
69
|
+
} else {
|
|
70
|
+
this.setMultiStateLegacyAsync({[this.dpClose]: true});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (this.currentStateTimeout) clearTimeout(this.currentStateTimeout);
|
|
74
|
+
this.currentStateTimeout = setTimeout(() => {
|
|
75
|
+
this.currentStateTimeout = null;
|
|
76
|
+
this.currentDoorState = value === Characteristic.TargetDoorState.OPEN
|
|
77
|
+
? Characteristic.CurrentDoorState.OPEN
|
|
78
|
+
: Characteristic.CurrentDoorState.CLOSED;
|
|
79
|
+
this.characteristicCurrentDoorState.updateValue(this.currentDoorState);
|
|
80
|
+
}, CURRENT_STATE_DELAY_MS);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
module.exports = SimpleGarageDoorAccessory;
|
|
@@ -29,6 +29,14 @@ class SimpleHeaterAccessory extends BaseAccessory {
|
|
|
29
29
|
this.temperatureDivisor = parseInt(this.device.context.temperatureDivisor) || 1;
|
|
30
30
|
this.thresholdTemperatureDivisor = parseInt(this.device.context.thresholdTemperatureDivisor) || 1;
|
|
31
31
|
this.temperatureOffset = parseInt(this.device.context.temperatureOffset) || 0;
|
|
32
|
+
// currentTemperatureOffset shifts only the displayed current temperature
|
|
33
|
+
// (not the threshold/setpoint). Use for devices whose onboard sensor
|
|
34
|
+
// reads systematically off from real ambient (e.g. wall-mounted patio
|
|
35
|
+
// heaters with sensor against the housing/wall). Defaults to
|
|
36
|
+
// temperatureOffset for backwards compatibility.
|
|
37
|
+
this.currentTemperatureOffset = this.device.context.currentTemperatureOffset !== undefined
|
|
38
|
+
? parseInt(this.device.context.currentTemperatureOffset)
|
|
39
|
+
: this.temperatureOffset;
|
|
32
40
|
|
|
33
41
|
const characteristicActive = service.getCharacteristic(Characteristic.Active)
|
|
34
42
|
.updateValue(this._getActive(dps[this.dpActive]))
|
|
@@ -50,8 +58,8 @@ class SimpleHeaterAccessory extends BaseAccessory {
|
|
|
50
58
|
.onSet(() => this.setStateAsync(this.dpActive, true));
|
|
51
59
|
|
|
52
60
|
const characteristicCurrentTemperature = service.getCharacteristic(Characteristic.CurrentTemperature)
|
|
53
|
-
.updateValue(this._getDividedState(dps[this.dpCurrentTemperature], this.temperatureDivisor))
|
|
54
|
-
.onGet(() => this.
|
|
61
|
+
.updateValue(this._getDividedState(dps[this.dpCurrentTemperature], this.temperatureDivisor, this.currentTemperatureOffset))
|
|
62
|
+
.onGet(() => this._getCurrentTempAsync());
|
|
55
63
|
|
|
56
64
|
const characteristicHeatingThresholdTemperature = service.getCharacteristic(Characteristic.HeatingThresholdTemperature)
|
|
57
65
|
.setProps({
|
|
@@ -79,7 +87,7 @@ class SimpleHeaterAccessory extends BaseAccessory {
|
|
|
79
87
|
}
|
|
80
88
|
|
|
81
89
|
if (changes.hasOwnProperty(this.dpCurrentTemperature) && characteristicCurrentTemperature.value !== changes[this.dpCurrentTemperature])
|
|
82
|
-
characteristicCurrentTemperature.updateValue(this._getDividedState(changes[this.dpCurrentTemperature], this.temperatureDivisor));
|
|
90
|
+
characteristicCurrentTemperature.updateValue(this._getDividedState(changes[this.dpCurrentTemperature], this.temperatureDivisor, this.currentTemperatureOffset));
|
|
83
91
|
|
|
84
92
|
characteristicCurrentHeaterCoolerState.updateValue(this._getCurrentHeaterCoolerState(state));
|
|
85
93
|
this.log.info('SimpleHeater changed: ' + JSON.stringify(state));
|
|
@@ -132,8 +140,15 @@ class SimpleHeaterAccessory extends BaseAccessory {
|
|
|
132
140
|
}
|
|
133
141
|
}
|
|
134
142
|
|
|
135
|
-
_getDividedState(dp, divisor) {
|
|
136
|
-
|
|
143
|
+
_getDividedState(dp, divisor, offsetOverride) {
|
|
144
|
+
const offset = offsetOverride !== undefined ? offsetOverride : this.temperatureOffset;
|
|
145
|
+
return ((parseFloat(dp) / divisor) + offset) || 0;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
_getCurrentTempAsync() {
|
|
149
|
+
const data = this.getStateAsync(this.dpCurrentTemperature);
|
|
150
|
+
if (!isFinite(data)) throw new Error('Invalid state');
|
|
151
|
+
return this._getDividedState(data, this.temperatureDivisor, this.currentTemperatureOffset);
|
|
137
152
|
}
|
|
138
153
|
}
|
|
139
154
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "homebridge-tuya-plus",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.11.0",
|
|
4
4
|
"description": "A community-maintained Homebridge plugin for controlling Tuya devices locally over LAN. Includes new features, fixes, and updated device support.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const SimpleGarageDoorAccessory = require('../lib/SimpleGarageDoorAccessory');
|
|
4
|
+
const { HAP, makeInstance } = require('./support/mocks');
|
|
5
|
+
|
|
6
|
+
const { CurrentDoorState: CDS, TargetDoorState: TDS } = HAP.Characteristic;
|
|
7
|
+
|
|
8
|
+
function makeSimpleGarage(contextOverrides = {}) {
|
|
9
|
+
const { instance, device, accessory, platform } = makeInstance(
|
|
10
|
+
SimpleGarageDoorAccessory,
|
|
11
|
+
{},
|
|
12
|
+
{ manufacturer: 'Generic', ...contextOverrides }
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
// Replicate what _registerCharacteristics would set up.
|
|
16
|
+
instance.dpOpen = '1';
|
|
17
|
+
instance.dpStop = '2';
|
|
18
|
+
instance.dpClose = '3';
|
|
19
|
+
instance.currentDoorState = CDS.OPEN;
|
|
20
|
+
instance.characteristicCurrentDoorState = {
|
|
21
|
+
value: CDS.OPEN,
|
|
22
|
+
updateValue: jest.fn().mockImplementation(function(v) { this.value = v; return this; }),
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
return { instance, device, accessory, platform };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// setTargetDoorState — device commands
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
describe('SimpleGarageDoorAccessory.setTargetDoorState — device commands', () => {
|
|
32
|
+
test('OPEN sends stop=true then open=true', () => {
|
|
33
|
+
const { instance, device } = makeSimpleGarage();
|
|
34
|
+
instance.setTargetDoorState(TDS.OPEN);
|
|
35
|
+
expect(device.update).toHaveBeenNthCalledWith(1, { '2': true });
|
|
36
|
+
expect(device.update).toHaveBeenNthCalledWith(2, { '1': true });
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('CLOSED sends stop=true then close=true', () => {
|
|
40
|
+
const { instance, device } = makeSimpleGarage();
|
|
41
|
+
instance.setTargetDoorState(TDS.CLOSED);
|
|
42
|
+
expect(device.update).toHaveBeenNthCalledWith(1, { '2': true });
|
|
43
|
+
expect(device.update).toHaveBeenNthCalledWith(2, { '3': true });
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('Skips writes when the device is disconnected', () => {
|
|
47
|
+
const { instance, device } = makeSimpleGarage();
|
|
48
|
+
device.connected = false;
|
|
49
|
+
instance.setTargetDoorState(TDS.OPEN);
|
|
50
|
+
expect(device.update).not.toHaveBeenCalled();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('Custom DPs are respected', () => {
|
|
54
|
+
const { instance, device } = makeSimpleGarage();
|
|
55
|
+
instance.dpOpen = '101';
|
|
56
|
+
instance.dpStop = '102';
|
|
57
|
+
instance.dpClose = '103';
|
|
58
|
+
instance.setTargetDoorState(TDS.OPEN);
|
|
59
|
+
expect(device.update).toHaveBeenNthCalledWith(1, { '102': true });
|
|
60
|
+
expect(device.update).toHaveBeenNthCalledWith(2, { '101': true });
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
// CurrentDoorState transition
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
describe('SimpleGarageDoorAccessory.setTargetDoorState — CurrentDoorState transition', () => {
|
|
68
|
+
beforeEach(() => jest.useFakeTimers());
|
|
69
|
+
afterEach(() => jest.useRealTimers());
|
|
70
|
+
|
|
71
|
+
test('CurrentDoorState lags the target by 1s when opening', () => {
|
|
72
|
+
const { instance } = makeSimpleGarage();
|
|
73
|
+
instance.currentDoorState = CDS.CLOSED;
|
|
74
|
+
instance.characteristicCurrentDoorState.value = CDS.CLOSED;
|
|
75
|
+
|
|
76
|
+
instance.setTargetDoorState(TDS.OPEN);
|
|
77
|
+
expect(instance.characteristicCurrentDoorState.value).toBe(CDS.CLOSED);
|
|
78
|
+
|
|
79
|
+
jest.advanceTimersByTime(999);
|
|
80
|
+
expect(instance.characteristicCurrentDoorState.value).toBe(CDS.CLOSED);
|
|
81
|
+
|
|
82
|
+
jest.advanceTimersByTime(1);
|
|
83
|
+
expect(instance.currentDoorState).toBe(CDS.OPEN);
|
|
84
|
+
expect(instance.characteristicCurrentDoorState.value).toBe(CDS.OPEN);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('CurrentDoorState lags the target by 1s when closing', () => {
|
|
88
|
+
const { instance } = makeSimpleGarage();
|
|
89
|
+
instance.currentDoorState = CDS.OPEN;
|
|
90
|
+
instance.characteristicCurrentDoorState.value = CDS.OPEN;
|
|
91
|
+
|
|
92
|
+
instance.setTargetDoorState(TDS.CLOSED);
|
|
93
|
+
expect(instance.characteristicCurrentDoorState.value).toBe(CDS.OPEN);
|
|
94
|
+
|
|
95
|
+
jest.advanceTimersByTime(1000);
|
|
96
|
+
expect(instance.currentDoorState).toBe(CDS.CLOSED);
|
|
97
|
+
expect(instance.characteristicCurrentDoorState.value).toBe(CDS.CLOSED);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('Reversing direction within the 1s window resets the timer', () => {
|
|
101
|
+
const { instance } = makeSimpleGarage();
|
|
102
|
+
instance.currentDoorState = CDS.OPEN;
|
|
103
|
+
instance.characteristicCurrentDoorState.value = CDS.OPEN;
|
|
104
|
+
|
|
105
|
+
instance.setTargetDoorState(TDS.CLOSED);
|
|
106
|
+
jest.advanceTimersByTime(500);
|
|
107
|
+
instance.setTargetDoorState(TDS.OPEN);
|
|
108
|
+
|
|
109
|
+
jest.advanceTimersByTime(500);
|
|
110
|
+
expect(instance.characteristicCurrentDoorState.value).toBe(CDS.OPEN);
|
|
111
|
+
|
|
112
|
+
jest.advanceTimersByTime(500);
|
|
113
|
+
expect(instance.currentDoorState).toBe(CDS.OPEN);
|
|
114
|
+
expect(instance.characteristicCurrentDoorState.value).toBe(CDS.OPEN);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
// Persistence
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
describe('SimpleGarageDoorAccessory persistence', () => {
|
|
122
|
+
test('Stores the latest target on the accessory context', () => {
|
|
123
|
+
const { instance, accessory } = makeSimpleGarage();
|
|
124
|
+
instance.setTargetDoorState(TDS.CLOSED);
|
|
125
|
+
expect(accessory.context.cachedTargetDoorState).toBe(TDS.CLOSED);
|
|
126
|
+
instance.setTargetDoorState(TDS.OPEN);
|
|
127
|
+
expect(accessory.context.cachedTargetDoorState).toBe(TDS.OPEN);
|
|
128
|
+
});
|
|
129
|
+
});
|