homebridge-tuya-without-developer-account 1.0.4 → 1.0.7

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 CHANGED
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.7
4
+
5
+ - Added Smart Pet Feeder support for `quick_feed`, `manual_feed`, `slow_feed`, `feed_state`, battery, and charging state.
6
+ - Added optional `deviceOverrides[].petFeeder.manualFeedAmount` and `deviceOverrides[].petFeeder.exposeSlowFeed`.
7
+ - Added Tuya alarm panel support as a HomeKit Security System using `master_mode`, `master_state`, and optional tamper/battery DPs.
8
+ - Added optional `deviceOverrides[].alarm` fields for alarm sound, muffling, and notification switches.
9
+ - Added clearer logging when Tuya returns an empty schema for aroma diffusers. Diffuser scenes remain exposed separately.
10
+
11
+ ## 1.0.6
12
+
13
+ - Fixed a Homebridge UI issue where clicking **Save Configuration** could leave the custom settings page spinner running indefinitely even when QR authentication data had already been saved.
14
+ - Added timeout handling around the custom UI save flow.
15
+ - Added post-save verification of the plugin config so users receive a clear success or recovery message instead of a permanent spinner.
16
+
17
+
18
+ ## 1.0.5
19
+
20
+ - Added support for DP10 / category `tgq` Tuya dimmer plugs that expose `switch_led` + `bright_value_v2`.
21
+ - Fixed dimmer validation so devices using `bright_value_v2` are exposed as HomeKit Lightbulb accessories with On and Brightness instead of being marked unsupported.
22
+ - Fixed dimmer on/off schema matching so `bright_value_v2` no longer incorrectly searches for `switch_v2` / `switch_led_v2`.
23
+
3
24
  ## 1.0.4
4
25
 
5
26
  - Added a Homebridge settings UI helper for air conditioner temperature overrides.
package/README.md CHANGED
@@ -4,6 +4,9 @@
4
4
 
5
5
  # Tuya without developer account for Homebridge
6
6
 
7
+ Current release: **1.0.6**
8
+
9
+
7
10
  A Homebridge platform plugin for Tuya and Smart Life devices that uses **Home Assistant-style Tuya QR Cloud Authentication**.
8
11
 
9
12
  This plugin is designed for users who want to add Tuya / Smart Life devices to HomeKit through Homebridge **without creating a Tuya IoT Developer Platform account** and without entering Tuya cloud project credentials.
@@ -305,3 +308,15 @@ Version 1.0.1 and later persist refreshed Tuya QR tokens back to the Homebridge
305
308
  ```
306
309
 
307
310
  If this still happens after upgrading, open the plugin settings, clear the saved authentication, generate a new QR code, scan it with the Tuya Smart or Smart Life app, save the configuration, and restart Homebridge. Also confirm the Homebridge host clock is synchronized, because Tuya signed requests depend on the current time.
311
+
312
+ ### DP10 Smart Dimmer Plug / `bright_value_v2` dimmers
313
+
314
+ Version **1.0.5** adds support for DP10-style Tuya dimmer plugs that expose `switch_led` and `bright_value_v2`. These are exposed in HomeKit as Lightbulb accessories with On and Brightness. If the accessory was previously shown as **Not Supported**, remove only that cached accessory in Homebridge UI and restart Homebridge after upgrading.
315
+
316
+
317
+
318
+ ## Version 1.0.7 device support
319
+
320
+ This release adds native support for Tuya Smart Pet Feeders and Tuya alarm panels that expose `master_mode`. Pet feeders expose quick/manual feed controls, optional slow-feed control, feed-state sensor, and battery when available. Alarm panels are exposed as HomeKit Security System accessories, with optional extra switches controlled through `deviceOverrides[].alarm`.
321
+
322
+ Aroma diffuser devices whose Tuya QR cloud schema is empty remain visible as unsupported direct devices, but any diffuser scenes returned by Tuya are still exposed separately.
@@ -204,3 +204,28 @@ Most category code is pinyin abbreviation of Chinese name.
204
204
 
205
205
 
206
206
  For the undocumented product category, you can try override it to the most similar one. See [ADVANCED_OPTIONS.md](./ADVANCED_OPTIONS.md).
207
+
208
+ ### DP10 / Treatlife Smart Dimmer Plug
209
+
210
+ Supported from **v1.0.5**. These devices normally report category `tgq` and expose:
211
+
212
+ - `switch_led` for On/Off
213
+ - `bright_value_v2` for Brightness
214
+
215
+ They are exposed to HomeKit as a Lightbulb with On and Brightness. After upgrading from an older version where the device showed as unsupported, remove the affected cached accessory from Homebridge UI and restart Homebridge.
216
+
217
+
218
+
219
+ ## Added in 1.0.7
220
+
221
+ ### Smart Pet Feeder
222
+
223
+ Supported when Tuya exposes one or more of: `quick_feed`, `manual_feed`, `slow_feed`, `feed_state`, `battery_percentage`, `charge_state`.
224
+
225
+ ### Alarm / Security System
226
+
227
+ Supported when Tuya exposes `master_mode`. Alarm-triggered state is detected from `master_state=alarm`, `sos_state=true`, or `master_mode=sos` when available. Optional extra switches can be enabled through `deviceOverrides[].alarm`.
228
+
229
+ ### Aroma Diffuser with Empty Schema
230
+
231
+ If Tuya QR cloud returns an empty schema for the diffuser device, direct device control cannot be mapped. The plugin keeps diffuser Tuya scenes exposed and logs a clearer explanation.
@@ -110,6 +110,49 @@
110
110
  "default": 1
111
111
  }
112
112
  }
113
+ },
114
+ "petFeeder": {
115
+ "type": "object",
116
+ "title": "Smart Pet Feeder Options",
117
+ "description": "Optional settings for Tuya pet feeders. The plugin exposes quick/manual feed switches, feed-state sensor, and battery when supported.",
118
+ "properties": {
119
+ "manualFeedAmount": {
120
+ "type": "integer",
121
+ "title": "Manual feed amount",
122
+ "description": "Portions sent when the Manual Feed switch is turned on.",
123
+ "minimum": 1,
124
+ "maximum": 12,
125
+ "default": 1
126
+ },
127
+ "exposeSlowFeed": {
128
+ "type": "boolean",
129
+ "title": "Expose Slow Feed switch",
130
+ "description": "Expose slow_feed as a HomeKit switch when the device supports it.",
131
+ "default": true
132
+ }
133
+ }
134
+ },
135
+ "alarm": {
136
+ "type": "object",
137
+ "title": "Alarm Options",
138
+ "description": "Optional extra switches for Tuya alarm panels. The main alarm is exposed as a HomeKit Security System when master_mode is available.",
139
+ "properties": {
140
+ "exposeAlarmSoundSwitch": {
141
+ "type": "boolean",
142
+ "title": "Expose Alarm Sound switch",
143
+ "default": false
144
+ },
145
+ "exposeMufflingSwitch": {
146
+ "type": "boolean",
147
+ "title": "Expose Mute/Muffling switch",
148
+ "default": false
149
+ },
150
+ "exposeNotificationSwitches": {
151
+ "type": "boolean",
152
+ "title": "Expose Call/SMS/App/Low Battery switches",
153
+ "default": false
154
+ }
155
+ }
113
156
  }
114
157
  },
115
158
  "required": [
package/dist/platform.js CHANGED
@@ -122,6 +122,34 @@ class TuyaPlatform {
122
122
  delete item.airConditioner;
123
123
  }
124
124
  }
125
+ if (item.petFeeder && typeof item.petFeeder === 'object') {
126
+ const normalizedPetFeeder = {};
127
+ const manualFeedAmount = Number(item.petFeeder.manualFeedAmount);
128
+ if (Number.isFinite(manualFeedAmount)) {
129
+ normalizedPetFeeder.manualFeedAmount = Math.max(1, Math.min(12, Math.round(manualFeedAmount)));
130
+ }
131
+ if (typeof item.petFeeder.exposeSlowFeed === 'boolean') {
132
+ normalizedPetFeeder.exposeSlowFeed = item.petFeeder.exposeSlowFeed;
133
+ }
134
+ if (Object.keys(normalizedPetFeeder).length > 0) {
135
+ item.petFeeder = normalizedPetFeeder;
136
+ } else {
137
+ delete item.petFeeder;
138
+ }
139
+ }
140
+ if (item.alarm && typeof item.alarm === 'object') {
141
+ const normalizedAlarm = {};
142
+ for (const key of ['exposeAlarmSoundSwitch', 'exposeMufflingSwitch', 'exposeNotificationSwitches']) {
143
+ if (typeof item.alarm[key] === 'boolean') {
144
+ normalizedAlarm[key] = item.alarm[key];
145
+ }
146
+ }
147
+ if (Object.keys(normalizedAlarm).length > 0) {
148
+ item.alarm = normalizedAlarm;
149
+ } else {
150
+ delete item.alarm;
151
+ }
152
+ }
125
153
  seenIds.add(id);
126
154
  validOverrides.push(item);
127
155
  }
@@ -249,6 +277,8 @@ class TuyaPlatform {
249
277
  unbridged: deviceConfig?.unbridged ?? false,
250
278
  schemaOverrides: deviceConfig?.schema ? JSON.stringify(deviceConfig.schema) : undefined,
251
279
  airConditioner: deviceConfig?.airConditioner ? JSON.stringify(deviceConfig.airConditioner) : undefined,
280
+ petFeeder: deviceConfig?.petFeeder ? JSON.stringify(deviceConfig.petFeeder) : undefined,
281
+ alarm: deviceConfig?.alarm ? JSON.stringify(deviceConfig.alarm) : undefined,
252
282
  adaptiveLighting: deviceConfig?.adaptiveLighting ?? false,
253
283
  };
254
284
  const { changed: configChanged } = this.configHash.hasConfigChanged(device.id, configToHash);
@@ -23,6 +23,13 @@ class DiffuserAccessory extends BaseAccessory_1.default {
23
23
  requiredSchema() {
24
24
  return [SCHEMA_CODE.SPRAY_ON];
25
25
  }
26
+
27
+ checkRequirements() {
28
+ if (this.device && Array.isArray(this.device.schema) && this.device.schema.length === 0) {
29
+ this.log.warn('Tuya returned an empty schema for this diffuser. Direct diffuser control cannot be mapped until Tuya exposes switch_spray or equivalent DPs. Any Tuya scenes for this diffuser will still be exposed separately.');
30
+ }
31
+ return super.checkRequirements();
32
+ }
26
33
  configureServices() {
27
34
  // Main Switch
28
35
  (0, On_1.configureOn)(this, undefined, this.getSchema(...SCHEMA_CODE.ON));
@@ -9,7 +9,7 @@ const Name_1 = require("./characteristic/Name");
9
9
  const On_1 = require("./characteristic/On");
10
10
  const SCHEMA_CODE = {
11
11
  ON: ['switch', 'switch_led', 'switch_1', 'switch_led_1'],
12
- BRIGHTNESS: ['bright_value', 'bright_value_1'],
12
+ BRIGHTNESS: ['bright_value', 'bright_value_v2', 'bright_value_1', 'bright_value_2', 'brightness'],
13
13
  };
14
14
  class DimmerAccessory extends BaseAccessory_1.default {
15
15
  requiredSchema() {
@@ -28,10 +28,31 @@ class DimmerAccessory extends BaseAccessory_1.default {
28
28
  const service = this.accessory.getService(_schema.code)
29
29
  || this.accessory.addService(this.Service.Lightbulb, name, _schema.code);
30
30
  (0, Name_1.configureName)(this, service, name);
31
- (0, On_1.configureOn)(this, service, this.getSchema('switch' + suffix, 'switch_led' + suffix));
31
+ const onSchema = this.resolveOnSchemaForBrightness(suffix);
32
+ if (!onSchema) {
33
+ this.log.warn(`No on/off schema found for brightness DP ${_schema.code}. Tried switch/switch_led variants.`);
34
+ }
35
+ (0, On_1.configureOn)(this, service, onSchema);
32
36
  this.configureBrightness(service, suffix);
33
37
  }
34
38
  }
39
+
40
+ resolveOnSchemaForBrightness(suffix) {
41
+ const candidates = [];
42
+ if (suffix) {
43
+ candidates.push('switch' + suffix, 'switch_led' + suffix);
44
+ }
45
+ // bright_value_v2 is a schema generation/version marker, not a separate channel suffix.
46
+ // Devices such as the Treatlife/DP10 Smart dimmer Plug expose:
47
+ // switch_led + bright_value_v2
48
+ // The older logic looked for switch_v2/switch_led_v2 and marked the device unsupported.
49
+ if (suffix === '_v2') {
50
+ candidates.push('switch_led', 'switch');
51
+ }
52
+ candidates.push('switch', 'switch_led', 'switch_1', 'switch_led_1');
53
+ return this.getSchema(...candidates);
54
+ }
55
+
35
56
  configureBrightness(service, suffix) {
36
57
  const schema = this.getSchema('bright_value' + suffix);
37
58
  if (!schema) {
@@ -4,136 +4,107 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const BaseAccessory_1 = __importDefault(require("./BaseAccessory"));
7
- const Active_1 = require("./characteristic/Active");
7
+ const Name_1 = require("./characteristic/Name");
8
8
  const SCHEMA_CODE = {
9
- ACTIVE: ['switch'],
10
- LIGHT: ['light'],
11
9
  QUICK_FEED: ['quick_feed'],
12
10
  SLOW_FEED: ['slow_feed'],
13
11
  MANUAL_FEED: ['manual_feed'],
14
- MEAL_PLAN: ['meal_plan'],
15
- BATTERY_PERCENTAGE: ['battery_percentage'],
16
- FEED_REPORT: ['feed_report'],
17
12
  FEED_STATE: ['feed_state'],
18
13
  };
19
14
  class PetFeederAccessory extends BaseAccessory_1.default {
20
15
  requiredSchema() {
21
- return [SCHEMA_CODE.ACTIVE];
16
+ // Tuya pet feeders often do not expose a generic "switch" DP. A feeder is
17
+ // considered supported when it has at least one command DP we can expose.
18
+ return [[...SCHEMA_CODE.QUICK_FEED, ...SCHEMA_CODE.MANUAL_FEED, ...SCHEMA_CODE.SLOW_FEED]];
19
+ }
20
+ getPetFeederConfig() {
21
+ const config = this.device ? this.platform.getDeviceConfig(this.device) : undefined;
22
+ const feeder = (config && typeof config.petFeeder === 'object') ? config.petFeeder : {};
23
+ const manualFeedAmount = Number(feeder.manualFeedAmount);
24
+ return {
25
+ manualFeedAmount: Number.isFinite(manualFeedAmount) ? Math.max(1, Math.min(12, Math.round(manualFeedAmount))) : 1,
26
+ exposeSlowFeed: feeder.exposeSlowFeed !== false,
27
+ };
22
28
  }
23
29
  configureServices() {
24
- (0, Active_1.configureActive)(this, this.mainService(), this.getSchema(...SCHEMA_CODE.ACTIVE));
25
- this.configureLight();
26
30
  this.configureQuickFeed();
27
- this.configureSlowFeed();
28
31
  this.configureManualFeed();
29
- this.configureMealPlan();
30
- this.configureBatteryPercentage();
31
- this.configureFeedReport();
32
+ this.configureSlowFeed();
32
33
  this.configureFeedState();
33
34
  }
34
- mainService() {
35
- return this.accessory.getService(this.Service.Switch)
36
- || this.accessory.addService(this.Service.Switch);
37
- }
38
- configureLight() {
39
- const schema = this.getSchema(...SCHEMA_CODE.LIGHT);
40
- if (!schema) {
41
- this.log.warn('Light is not supported.');
42
- return;
43
- }
44
- this.mainService().getCharacteristic(this.Characteristic.On)
45
- .onSet(async (value) => {
46
- await this.sendCommands([{ code: schema.code, value: value }]);
47
- });
48
- }
49
- configureQuickFeed() {
50
- const schema = this.getSchema(...SCHEMA_CODE.QUICK_FEED);
35
+ configureActionSwitch(schema, name, subtype, onSet) {
51
36
  if (!schema) {
52
- this.log.warn('Quick feed is not supported.');
53
37
  return;
54
38
  }
55
- this.mainService().getCharacteristic(this.Characteristic.On)
39
+ const service = this.accessory.getServiceById(this.Service.Switch, subtype)
40
+ || this.accessory.addService(this.Service.Switch, name, subtype);
41
+ (0, Name_1.configureName)(this, service, name);
42
+ service.getCharacteristic(this.Characteristic.On)
43
+ .onGet(() => {
44
+ this.checkOnlineStatus();
45
+ // quick_feed/manual_feed are momentary actions. Always display them as off.
46
+ return false;
47
+ })
56
48
  .onSet(async (value) => {
57
49
  if (value) {
58
- await this.sendCommands([{ code: schema.code, value: true }]);
50
+ await onSet();
59
51
  }
52
+ setTimeout(() => service.getCharacteristic(this.Characteristic.On).updateValue(false), 500);
60
53
  });
61
54
  }
62
- configureSlowFeed() {
63
- const schema = this.getSchema(...SCHEMA_CODE.SLOW_FEED);
55
+ configureBooleanSwitch(schema, name, subtype) {
64
56
  if (!schema) {
65
- this.log.warn('Slow feed is not supported.');
66
57
  return;
67
58
  }
68
- this.mainService().getCharacteristic(this.Characteristic.On)
69
- .onSet(async (value) => {
70
- if (value) {
71
- await this.sendCommands([{ code: schema.code, value: true }]);
72
- }
73
- });
74
- }
75
- configureManualFeed() {
76
- const schema = this.getSchema(...SCHEMA_CODE.MANUAL_FEED);
77
- if (!schema) {
78
- this.log.warn('Manual feed is not supported.');
79
- return;
80
- }
81
- this.mainService().getCharacteristic(this.Characteristic.On)
59
+ const service = this.accessory.getServiceById(this.Service.Switch, subtype)
60
+ || this.accessory.addService(this.Service.Switch, name, subtype);
61
+ (0, Name_1.configureName)(this, service, name);
62
+ service.getCharacteristic(this.Characteristic.On)
63
+ .onGet(() => {
64
+ this.checkOnlineStatus();
65
+ return !!(this.getStatus(schema.code)?.value ?? false);
66
+ })
82
67
  .onSet(async (value) => {
83
- if (value) {
84
- await this.sendCommands([{ code: schema.code, value: 1 }]);
85
- }
68
+ await this.sendCommands([{ code: schema.code, value: !!value }], true);
86
69
  });
87
70
  }
88
- configureMealPlan() {
89
- const schema = this.getSchema(...SCHEMA_CODE.MEAL_PLAN);
90
- if (!schema) {
91
- this.log.warn('Meal plan is not supported.');
92
- return;
93
- }
94
- this.mainService().getCharacteristic(this.Characteristic.On)
95
- .onSet(async (value) => {
96
- if (value) {
97
- await this.sendCommands([{ code: schema.code, value: value }]);
98
- }
71
+ configureQuickFeed() {
72
+ const schema = this.getSchema(...SCHEMA_CODE.QUICK_FEED);
73
+ this.configureActionSwitch(schema, `${this.device?.name || 'Pet Feeder'} Quick Feed`, 'quick_feed', async () => {
74
+ await this.sendCommands([{ code: schema.code, value: true }], true);
99
75
  });
100
76
  }
101
- configureBatteryPercentage() {
102
- const schema = this.getSchema(...SCHEMA_CODE.BATTERY_PERCENTAGE);
103
- if (!schema) {
104
- this.log.warn('Battery percentage is not supported.');
105
- return;
106
- }
107
- this.mainService().getCharacteristic(this.Characteristic.BatteryLevel)
108
- .onGet(() => {
109
- const status = this.getStatus(schema.code);
110
- return status.value;
77
+ configureManualFeed() {
78
+ const schema = this.getSchema(...SCHEMA_CODE.MANUAL_FEED);
79
+ const { manualFeedAmount } = this.getPetFeederConfig();
80
+ this.configureActionSwitch(schema, `${this.device?.name || 'Pet Feeder'} Manual Feed`, 'manual_feed', async () => {
81
+ await this.sendCommands([{ code: schema.code, value: manualFeedAmount }], true);
111
82
  });
112
83
  }
113
- configureFeedReport() {
114
- const schema = this.getSchema(...SCHEMA_CODE.FEED_REPORT);
115
- if (!schema) {
116
- this.log.warn('Feed report is not supported.');
84
+ configureSlowFeed() {
85
+ const schema = this.getSchema(...SCHEMA_CODE.SLOW_FEED);
86
+ const { exposeSlowFeed } = this.getPetFeederConfig();
87
+ if (!exposeSlowFeed) {
117
88
  return;
118
89
  }
119
- this.mainService().getCharacteristic(this.Characteristic.StatusActive)
120
- .onGet(() => {
121
- const status = this.getStatus(schema.code);
122
- return status.value;
123
- });
90
+ this.configureBooleanSwitch(schema, `${this.device?.name || 'Pet Feeder'} Slow Feed`, 'slow_feed');
124
91
  }
125
92
  configureFeedState() {
126
93
  const schema = this.getSchema(...SCHEMA_CODE.FEED_STATE);
127
94
  if (!schema) {
128
- this.log.warn('Feed state is not supported.');
129
95
  return;
130
96
  }
131
- this.mainService().getCharacteristic(this.Characteristic.StatusActive)
97
+ const service = this.accessory.getServiceById(this.Service.OccupancySensor, 'feed_state')
98
+ || this.accessory.addService(this.Service.OccupancySensor, `${this.device?.name || 'Pet Feeder'} Feeding`, 'feed_state');
99
+ (0, Name_1.configureName)(this, service, `${this.device?.name || 'Pet Feeder'} Feeding`);
100
+ const { OCCUPANCY_DETECTED, OCCUPANCY_NOT_DETECTED } = this.Characteristic.OccupancyDetected;
101
+ service.getCharacteristic(this.Characteristic.OccupancyDetected)
132
102
  .onGet(() => {
133
- const status = this.getStatus(schema.code);
134
- return status.value === 'feeding';
103
+ this.checkOnlineStatus();
104
+ const value = this.getStatus(schema.code)?.value;
105
+ return value === 'feeding' ? OCCUPANCY_DETECTED : OCCUPANCY_NOT_DETECTED;
135
106
  });
136
107
  }
137
108
  }
138
109
  exports.default = PetFeederAccessory;
139
- //# sourceMappingURL=PetFeederAccessory.js.map
110
+ //# sourceMappingURL=PetFeederAccessory.js.map
@@ -4,11 +4,19 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const BaseAccessory_1 = __importDefault(require("./BaseAccessory"));
7
- const SecuritySystemState_1 = require("./characteristic/SecuritySystemState");
8
7
  const Name_1 = require("./characteristic/Name");
9
8
  const SCHEMA_CODE = {
10
9
  MASTER_MODE: ['master_mode'],
10
+ MASTER_STATE: ['master_state'],
11
11
  SOS_STATE: ['sos_state'],
12
+ TAMPER_ALARM: ['temper_alarm', 'tamper_alarm'],
13
+ ALARM_SOUND: ['switch_alarm_sound'],
14
+ MUFFLING: ['muffling'],
15
+ ALARM_CALL: ['switch_alarm_call'],
16
+ ALARM_SMS: ['switch_alarm_sms'],
17
+ ALARM_PROPEL: ['switch_alarm_propel'],
18
+ LOW_BATTERY_ALERT: ['switch_low_battery'],
19
+ MODE_DELAY_SOUND: ['switch_mode_dl_sound'],
12
20
  };
13
21
  class SecuritySystemAccessory extends BaseAccessory_1.default {
14
22
  constructor() {
@@ -16,15 +24,146 @@ class SecuritySystemAccessory extends BaseAccessory_1.default {
16
24
  this.isNightArm = false;
17
25
  }
18
26
  requiredSchema() {
19
- return [SCHEMA_CODE.MASTER_MODE, SCHEMA_CODE.SOS_STATE];
27
+ // Some Tuya alarm panels expose master_mode + master_state, but not sos_state.
28
+ return [SCHEMA_CODE.MASTER_MODE];
29
+ }
30
+ getAlarmConfig() {
31
+ const config = this.device ? this.platform.getDeviceConfig(this.device) : undefined;
32
+ const alarm = (config && typeof config.alarm === 'object') ? config.alarm : {};
33
+ return {
34
+ exposeAlarmSoundSwitch: !!alarm.exposeAlarmSoundSwitch,
35
+ exposeMufflingSwitch: !!alarm.exposeMufflingSwitch,
36
+ exposeNotificationSwitches: !!alarm.exposeNotificationSwitches,
37
+ };
20
38
  }
21
39
  configureServices() {
22
40
  const service = this.accessory.getService(this.Service.SecuritySystem)
23
41
  || this.accessory.addService(this.Service.SecuritySystem);
24
42
  (0, Name_1.configureName)(this, service, this.device.name);
25
- (0, SecuritySystemState_1.configureSecuritySystemCurrentState)(this, service, this.getSchema(...SCHEMA_CODE.MASTER_MODE), this.getSchema(...SCHEMA_CODE.SOS_STATE));
26
- (0, SecuritySystemState_1.configureSecuritySystemTargetState)(this, service, this.getSchema(...SCHEMA_CODE.MASTER_MODE), this.getSchema(...SCHEMA_CODE.SOS_STATE));
43
+ this.configureCurrentState(service);
44
+ this.configureTargetState(service);
45
+ this.configureTamper(service);
46
+ this.configureExtraSwitches();
47
+ }
48
+ mapTuyaModeToHomeKit(value, current = true) {
49
+ const Current = this.Characteristic.SecuritySystemCurrentState;
50
+ const Target = this.Characteristic.SecuritySystemTargetState;
51
+ const map = current ? {
52
+ disarmed: Current.DISARMED,
53
+ arm: Current.AWAY_ARM,
54
+ home: this.isNightArm ? Current.NIGHT_ARM : Current.STAY_ARM,
55
+ sos: Current.ALARM_TRIGGERED,
56
+ } : {
57
+ disarmed: Target.DISARM,
58
+ arm: Target.AWAY_ARM,
59
+ home: this.isNightArm ? Target.NIGHT_ARM : Target.STAY_ARM,
60
+ sos: Target.AWAY_ARM,
61
+ };
62
+ return map[value] ?? (current ? Current.DISARMED : Target.DISARM);
63
+ }
64
+ mapHomeKitTargetToTuya(value) {
65
+ const Target = this.Characteristic.SecuritySystemTargetState;
66
+ switch (value) {
67
+ case Target.DISARM:
68
+ return 'disarmed';
69
+ case Target.STAY_ARM:
70
+ case Target.NIGHT_ARM:
71
+ return 'home';
72
+ case Target.AWAY_ARM:
73
+ default:
74
+ return 'arm';
75
+ }
76
+ }
77
+ isAlarmTriggered() {
78
+ const masterStateSchema = this.getSchema(...SCHEMA_CODE.MASTER_STATE);
79
+ if (masterStateSchema && this.getStatus(masterStateSchema.code)?.value === 'alarm') {
80
+ return true;
81
+ }
82
+ const sosStateSchema = this.getSchema(...SCHEMA_CODE.SOS_STATE);
83
+ if (sosStateSchema && this.getStatus(sosStateSchema.code)?.value) {
84
+ return true;
85
+ }
86
+ const masterModeSchema = this.getSchema(...SCHEMA_CODE.MASTER_MODE);
87
+ if (masterModeSchema && this.getStatus(masterModeSchema.code)?.value === 'sos') {
88
+ return true;
89
+ }
90
+ return false;
91
+ }
92
+ configureCurrentState(service) {
93
+ const masterModeSchema = this.getSchema(...SCHEMA_CODE.MASTER_MODE);
94
+ service.getCharacteristic(this.Characteristic.SecuritySystemCurrentState)
95
+ .onGet(() => {
96
+ this.checkOnlineStatus();
97
+ if (this.isAlarmTriggered()) {
98
+ return this.Characteristic.SecuritySystemCurrentState.ALARM_TRIGGERED;
99
+ }
100
+ return this.mapTuyaModeToHomeKit(this.getStatus(masterModeSchema.code)?.value, true);
101
+ });
102
+ }
103
+ configureTargetState(service) {
104
+ const masterModeSchema = this.getSchema(...SCHEMA_CODE.MASTER_MODE);
105
+ service.getCharacteristic(this.Characteristic.SecuritySystemTargetState)
106
+ .onGet(() => {
107
+ this.checkOnlineStatus();
108
+ return this.mapTuyaModeToHomeKit(this.getStatus(masterModeSchema.code)?.value, false);
109
+ })
110
+ .onSet(async (value) => {
111
+ this.isNightArm = value === this.Characteristic.SecuritySystemTargetState.NIGHT_ARM;
112
+ const commands = [{ code: masterModeSchema.code, value: this.mapHomeKitTargetToTuya(value) }];
113
+ const sosStateSchema = this.getSchema(...SCHEMA_CODE.SOS_STATE);
114
+ if (sosStateSchema && value === this.Characteristic.SecuritySystemTargetState.DISARM) {
115
+ commands.push({ code: sosStateSchema.code, value: false });
116
+ }
117
+ await this.sendCommands(commands, true);
118
+ });
119
+ }
120
+ configureTamper(service) {
121
+ const schema = this.getSchema(...SCHEMA_CODE.TAMPER_ALARM);
122
+ if (!schema) {
123
+ return;
124
+ }
125
+ if (!service.testCharacteristic(this.Characteristic.StatusTampered)) {
126
+ service.addOptionalCharacteristic(this.Characteristic.StatusTampered);
127
+ }
128
+ const { TAMPERED, NOT_TAMPERED } = this.Characteristic.StatusTampered;
129
+ service.getCharacteristic(this.Characteristic.StatusTampered)
130
+ .onGet(() => {
131
+ this.checkOnlineStatus();
132
+ return this.getStatus(schema.code)?.value ? TAMPERED : NOT_TAMPERED;
133
+ });
134
+ }
135
+ configureExtraSwitches() {
136
+ const config = this.getAlarmConfig();
137
+ if (config.exposeAlarmSoundSwitch) {
138
+ this.configureBooleanSwitch(this.getSchema(...SCHEMA_CODE.ALARM_SOUND), `${this.device.name} Alarm Sound`, 'switch_alarm_sound');
139
+ }
140
+ if (config.exposeMufflingSwitch) {
141
+ this.configureBooleanSwitch(this.getSchema(...SCHEMA_CODE.MUFFLING), `${this.device.name} Mute`, 'muffling');
142
+ }
143
+ if (config.exposeNotificationSwitches) {
144
+ this.configureBooleanSwitch(this.getSchema(...SCHEMA_CODE.ALARM_CALL), `${this.device.name} Alarm Call`, 'switch_alarm_call');
145
+ this.configureBooleanSwitch(this.getSchema(...SCHEMA_CODE.ALARM_SMS), `${this.device.name} Alarm SMS`, 'switch_alarm_sms');
146
+ this.configureBooleanSwitch(this.getSchema(...SCHEMA_CODE.ALARM_PROPEL), `${this.device.name} App Push`, 'switch_alarm_propel');
147
+ this.configureBooleanSwitch(this.getSchema(...SCHEMA_CODE.LOW_BATTERY_ALERT), `${this.device.name} Low Battery Alert`, 'switch_low_battery');
148
+ this.configureBooleanSwitch(this.getSchema(...SCHEMA_CODE.MODE_DELAY_SOUND), `${this.device.name} Mode Delay Sound`, 'switch_mode_dl_sound');
149
+ }
150
+ }
151
+ configureBooleanSwitch(schema, name, subtype) {
152
+ if (!schema) {
153
+ return;
154
+ }
155
+ const service = this.accessory.getServiceById(this.Service.Switch, subtype)
156
+ || this.accessory.addService(this.Service.Switch, name, subtype);
157
+ (0, Name_1.configureName)(this, service, name);
158
+ service.getCharacteristic(this.Characteristic.On)
159
+ .onGet(() => {
160
+ this.checkOnlineStatus();
161
+ return !!(this.getStatus(schema.code)?.value ?? false);
162
+ })
163
+ .onSet(async (value) => {
164
+ await this.sendCommands([{ code: schema.code, value: !!value }], true);
165
+ });
27
166
  }
28
167
  }
29
168
  exports.default = SecuritySystemAccessory;
30
- //# sourceMappingURL=SecuritySystemAccessory.js.map
169
+ //# sourceMappingURL=SecuritySystemAccessory.js.map
@@ -554,15 +554,92 @@
554
554
  }
555
555
  }
556
556
 
557
+
558
+ function delay(ms) {
559
+ return new Promise((resolve) => setTimeout(resolve, ms));
560
+ }
561
+
562
+ async function withTimeout(promise, timeoutMs, timeoutMessage) {
563
+ let timer;
564
+ try {
565
+ return await Promise.race([
566
+ promise,
567
+ new Promise((_, reject) => {
568
+ timer = setTimeout(() => reject(new Error(timeoutMessage)), timeoutMs);
569
+ }),
570
+ ]);
571
+ } finally {
572
+ if (timer) {
573
+ clearTimeout(timer);
574
+ }
575
+ }
576
+ }
577
+
578
+ async function verifySavedConfig(userCode) {
579
+ const blocks = await withTimeout(
580
+ homebridge.getPluginConfig(),
581
+ 8000,
582
+ 'Configuration save timed out while verifying the saved plugin config.'
583
+ );
584
+ const block = Array.isArray(blocks) && blocks.length > 0 ? blocks[0] : blocks;
585
+ return !!(block && block.platform === PLATFORM && block.options && block.options.userCode === userCode);
586
+ }
587
+
557
588
  async function saveConfig() {
558
589
  if (!isAuthenticated) {
559
590
  setStatus('Scan and approve the QR code before saving.', 'warning');
560
591
  return;
561
592
  }
593
+
594
+ const userCode = getUserCode();
595
+ if (!userCode) {
596
+ setStatus('User Code is required before saving.', 'warning');
597
+ return;
598
+ }
599
+
562
600
  try {
563
601
  homebridge.showSpinner();
564
- await syncConfigToUi();
565
- await homebridge.savePluginConfig();
602
+ $('tuyaNodevSave').disabled = true;
603
+
604
+ await withTimeout(
605
+ syncConfigToUi(),
606
+ 8000,
607
+ 'Timed out while preparing the plugin configuration for saving.'
608
+ );
609
+
610
+ // Some Homebridge UI versions have been observed to save the config but never resolve
611
+ // savePluginConfig(), which leaves the custom UI spinner running forever. Timeout the
612
+ // UI call, then verify the saved config before showing the final result.
613
+ let saveTimedOut = false;
614
+ try {
615
+ await withTimeout(
616
+ homebridge.savePluginConfig(),
617
+ 15000,
618
+ 'Homebridge UI did not finish the save request in time.'
619
+ );
620
+ } catch (e) {
621
+ saveTimedOut = true;
622
+ }
623
+
624
+ let verified = false;
625
+ try {
626
+ await delay(750);
627
+ verified = await verifySavedConfig(userCode);
628
+ } catch (e) {
629
+ verified = false;
630
+ }
631
+
632
+ if (saveTimedOut && !verified) {
633
+ throw new Error('Save did not complete. The QR auth token is saved, but the plugin configuration could not be verified. Close this window, refresh Homebridge UI, and check whether the config was saved.');
634
+ }
635
+
636
+ if (saveTimedOut && verified) {
637
+ const message = 'Configuration appears to be saved, but Homebridge UI did not return a save confirmation. Close this settings window and restart Homebridge.';
638
+ homebridge.toast.success(message, 'Tuya');
639
+ setStatus(message, 'success');
640
+ return;
641
+ }
642
+
566
643
  homebridge.toast.success('Configuration saved. Restart Homebridge to load devices.', 'Tuya');
567
644
  setStatus('Configuration saved. Restart Homebridge to load devices.', 'success');
568
645
  } catch (e) {
@@ -570,6 +647,9 @@
570
647
  homebridge.toast.error(e.message || 'Failed to save configuration.', 'Tuya');
571
648
  } finally {
572
649
  homebridge.hideSpinner();
650
+ if (isAuthenticated) {
651
+ $('tuyaNodevSave').disabled = false;
652
+ }
573
653
  }
574
654
  }
575
655
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "homebridge-tuya-without-developer-account",
3
3
  "displayName": "Tuya without developer account for Homebridge",
4
- "version": "1.0.4",
4
+ "version": "1.0.7",
5
5
  "description": "Homebridge plugin for Tuya and Smart Life devices using QR cloud authentication without a Tuya IoT developer account.",
6
6
  "license": "MIT",
7
7
  "author": "Kosztyk",