@homebridge-plugins/homebridge-ecovacs 7.2.2 → 7.2.4-beta.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/CHANGELOG.md CHANGED
@@ -2,6 +2,19 @@
2
2
 
3
3
  All notable changes to homebridge-ecovacs will be documented in this file.
4
4
 
5
+ ## v7.2.4 (Unreleased)
6
+
7
+ ### Changes
8
+
9
+ - updated dependencies
10
+ - option to expose as a vacuum robot in homekit
11
+
12
+ ## v7.2.3 (2025-07-24)
13
+
14
+ ### Other Changes
15
+
16
+ - dependency updates
17
+
5
18
  ## v7.2.2 (2025-07-18)
6
19
 
7
20
  ### Other Changes
package/lib/platform.js CHANGED
@@ -13,6 +13,7 @@ const require = createRequire(import.meta.url)
13
13
  const plugin = require('../package.json')
14
14
 
15
15
  const devicesInHB = new Map()
16
+ const matterDevicesInHB = new Map()
16
17
 
17
18
  export default class {
18
19
  constructor(log, config, api) {
@@ -26,6 +27,9 @@ export default class {
26
27
  this.log = log
27
28
  this.isBeta = plugin.version.includes('beta')
28
29
 
30
+ // Check if Matter is available and enabled
31
+ this.matterEnabled = api.isMatterAvailable?.() && api.isMatterEnabled?.()
32
+
29
33
  // Configuration objects for accessories
30
34
  this.deviceConf = {}
31
35
  this.ignoredDevices = []
@@ -374,6 +378,13 @@ export default class {
374
378
  // Display version of the ecovacs-deebot library in the log
375
379
  this.log('%s v%s.', platformLang.ecovacsLibVersion, this.ecovacsAPI.getVersion())
376
380
 
381
+ // Log Matter availability
382
+ if (this.matterEnabled) {
383
+ this.log('Matter support is enabled - vacuums will be exposed via both HAP and Matter.')
384
+ } else {
385
+ this.log('Matter support is not available - vacuums will be exposed via HAP only.')
386
+ }
387
+
377
388
  // Attempt to log in to Ecovacs/Yeedi
378
389
  try {
379
390
  await this.ecovacsAPI.connect(this.config.username, EcoVacsAPI.md5(this.config.password))
@@ -440,6 +451,230 @@ export default class {
440
451
  }
441
452
  }
442
453
 
454
+ createMatterRVCAccessory(device, accessory) {
455
+ // Create a Matter RVC (Robotic Vacuum Cleaner) accessory config
456
+ const serialNumber = device.did
457
+ const displayName = device.nick || device.did
458
+
459
+ // Get device types we need
460
+ const RoboticVacuumCleanerType = this.api.matter.deviceTypes.RoboticVacuumCleaner
461
+
462
+ // Create bound handler functions that preserve the platform context
463
+ const boundHandlers = {
464
+ rvcRunMode: {
465
+ changeToMode: (request) => {
466
+ return this.handleMatterRunModeChange(accessory, request)
467
+ },
468
+ },
469
+ rvcOperationalState: {
470
+ pause: () => {
471
+ return this.handleMatterPause(accessory)
472
+ },
473
+ stop: () => {
474
+ return this.handleMatterStop(accessory)
475
+ },
476
+ start: () => {
477
+ return this.handleMatterStart(accessory)
478
+ },
479
+ resume: () => {
480
+ return this.handleMatterResume(accessory)
481
+ },
482
+ goHome: () => {
483
+ return this.handleMatterGoHome(accessory)
484
+ },
485
+ },
486
+ }
487
+
488
+ return {
489
+ uuid: this.api.matter.uuid.generate(serialNumber),
490
+ displayName,
491
+ deviceType: RoboticVacuumCleanerType,
492
+ serialNumber,
493
+ manufacturer: device.company || 'Ecovacs',
494
+ model: device.deviceModel || 'Deebot',
495
+ firmwareRevision: '1.0.0',
496
+ hardwareRevision: '1.0.0',
497
+
498
+ context: {
499
+ hapAccessoryUUID: accessory.UUID,
500
+ deviceId: device.did,
501
+ },
502
+
503
+ clusters: {
504
+ // Run Mode: Idle or Cleaning
505
+ rvcRunMode: {
506
+ supportedModes: [
507
+ { label: 'Idle', mode: 0, modeTags: [{ value: 16384 }] }, // RvcRunMode.ModeTag.Idle
508
+ { label: 'Cleaning', mode: 1, modeTags: [{ value: 16385 }] }, // RvcRunMode.ModeTag.Cleaning
509
+ ],
510
+ currentMode: 0, // Start as Idle
511
+ },
512
+ // Clean Mode: Just Vacuum mode
513
+ rvcCleanMode: {
514
+ supportedModes: [
515
+ { label: 'Vacuum', mode: 0, modeTags: [{ value: 16385 }] }, // RvcCleanMode.ModeTag.Vacuum
516
+ ],
517
+ currentMode: 0,
518
+ },
519
+ // Operational State
520
+ rvcOperationalState: {
521
+ operationalStateList: [
522
+ { operationalStateId: 0, operationalStateLabel: 'Stopped' },
523
+ { operationalStateId: 1, operationalStateLabel: 'Running' },
524
+ { operationalStateId: 2, operationalStateLabel: 'Paused' },
525
+ { operationalStateId: 3, operationalStateLabel: 'Error' },
526
+ { operationalStateId: 64, operationalStateLabel: 'Seeking Charger' },
527
+ { operationalStateId: 65, operationalStateLabel: 'Charging' },
528
+ { operationalStateId: 66, operationalStateLabel: 'Docked' },
529
+ ],
530
+ operationalState: 66, // Start docked
531
+ },
532
+ },
533
+
534
+ handlers: boundHandlers,
535
+ }
536
+ }
537
+
538
+ async handleMatterRunModeChange(accessory, request) {
539
+ try {
540
+ const { newMode } = request
541
+ accessory.logDebug(`Matter: ChangeToMode (run) request received: mode=${newMode}`)
542
+
543
+ if (newMode === 1) {
544
+ // Switching to Cleaning mode - start cleaning
545
+ await this.internalCleanUpdate(accessory, true)
546
+ } else if (newMode === 0) {
547
+ // Switching to Idle mode - stop or return to dock
548
+ await this.internalChargeUpdate(accessory, true)
549
+ }
550
+ } catch (err) {
551
+ accessory.logWarn(`Matter: Run mode change failed ${parseError(err)}`)
552
+ }
553
+ }
554
+
555
+ async handleMatterPause(accessory) {
556
+ try {
557
+ accessory.logDebug('Matter: Pause request received')
558
+ // Stop the vacuum
559
+ // TODO work out which devices have a pause and implement properly
560
+ await this.internalCleanUpdate(accessory, false)
561
+
562
+ // Update Matter states immediately
563
+ this.updateMatterRunMode(accessory, 0) // idle
564
+ this.updateMatterOperationalState(accessory, 0) // stopped
565
+ } catch (err) {
566
+ accessory.logWarn(`Matter: Pause failed ${parseError(err)}`)
567
+ }
568
+ }
569
+
570
+ async handleMatterStop(accessory) {
571
+ try {
572
+ accessory.logDebug('Matter: Stop request received')
573
+ await this.internalCleanUpdate(accessory, false)
574
+
575
+ // Update Matter states immediately
576
+ this.updateMatterRunMode(accessory, 0) // idle
577
+ this.updateMatterOperationalState(accessory, 0) // stopped
578
+ } catch (err) {
579
+ accessory.logWarn(`Matter: Stop failed ${parseError(err)}`)
580
+ }
581
+ }
582
+
583
+ async handleMatterStart(accessory) {
584
+ try {
585
+ accessory.logDebug('Matter: Start request received')
586
+ await this.internalCleanUpdate(accessory, true)
587
+ } catch (err) {
588
+ accessory.logWarn(`Matter: Start failed ${parseError(err)}`)
589
+ }
590
+ }
591
+
592
+ async handleMatterResume(accessory) {
593
+ try {
594
+ accessory.logDebug('Matter: Resume request received')
595
+ await this.internalCleanUpdate(accessory, true)
596
+ } catch (err) {
597
+ accessory.logWarn(`Matter: Resume failed ${parseError(err)}`)
598
+ }
599
+ }
600
+
601
+ async handleMatterGoHome(accessory) {
602
+ try {
603
+ accessory.logDebug('Matter: GoHome request received')
604
+ await this.internalChargeUpdate(accessory, true)
605
+ } catch (err) {
606
+ accessory.logWarn(`Matter: GoHome failed ${parseError(err)}`)
607
+ }
608
+ }
609
+
610
+ updateMatterOperationalState(accessory, state) {
611
+ // Update Matter operational state if Matter is enabled and accessory has Matter UUID
612
+ if (!this.matterEnabled || !accessory.matterUUID || !accessory.matterReady) {
613
+ return
614
+ }
615
+
616
+ try {
617
+ this.api.matter.updateAccessoryState(accessory.matterUUID, 'rvcOperationalState', {
618
+ operationalState: state,
619
+ })
620
+ accessory.logDebug(`Matter: Operational state updated to ${state}`)
621
+ } catch (err) {
622
+ accessory.logError(`Matter: Failed to update operational state: ${parseError(err)}`)
623
+ }
624
+ }
625
+
626
+ updateMatterRunMode(accessory, mode) {
627
+ // Update Matter run mode if Matter is enabled and accessory has Matter UUID
628
+ if (!this.matterEnabled || !accessory.matterUUID || !accessory.matterReady) {
629
+ return
630
+ }
631
+
632
+ try {
633
+ this.api.matter.updateAccessoryState(accessory.matterUUID, 'rvcRunMode', {
634
+ currentMode: mode,
635
+ })
636
+ accessory.logDebug(`Matter: Run mode updated to ${mode}`)
637
+ } catch (err) {
638
+ accessory.logError(`Matter: Failed to update run mode: ${parseError(err)}`)
639
+ }
640
+ }
641
+
642
+ updateMatterStateFromHAP(accessory) {
643
+ // Sync current HAP state to Matter when Matter becomes ready
644
+ if (!this.matterEnabled || !accessory.matterUUID || !accessory.matterReady) {
645
+ return
646
+ }
647
+
648
+ try {
649
+ // Sync cleaning state
650
+ const isActive = ['auto', 'clean', 'edge', 'spot', 'spotarea', 'customarea'].includes(
651
+ (accessory.context.cacheClean || '').toLowerCase().replace(/[^a-z]+/g, ''),
652
+ )
653
+ if (isActive) {
654
+ this.updateMatterRunMode(accessory, 1) // Cleaning
655
+ this.updateMatterOperationalState(accessory, 1) // Running
656
+ } else {
657
+ this.updateMatterRunMode(accessory, 0) // Idle
658
+ }
659
+
660
+ // Sync charging state
661
+ const chargeStateLower = (accessory.context.cacheCharge || '').toLowerCase()
662
+ if (chargeStateLower === 'charging') {
663
+ this.updateMatterOperationalState(accessory, 65) // charging
664
+ } else if (chargeStateLower === 'returning') {
665
+ this.updateMatterOperationalState(accessory, 64) // seeking charger
666
+ } else if (chargeStateLower === 'idle') {
667
+ this.updateMatterOperationalState(accessory, 66) // docked
668
+ } else if (!isActive) {
669
+ this.updateMatterOperationalState(accessory, 0) // stopped
670
+ }
671
+
672
+ accessory.logDebug('Matter: Initial state synchronized from HAP')
673
+ } catch (err) {
674
+ accessory.logError(`Matter: Failed to sync initial state: ${parseError(err)}`)
675
+ }
676
+ }
677
+
443
678
  initialiseDevice(device) {
444
679
  try {
445
680
  // Generate the Homebridge UUID from the device id
@@ -789,6 +1024,49 @@ export default class {
789
1024
  this.api.updatePlatformAccessories([accessory])
790
1025
  devicesInHB.set(accessory.UUID, accessory)
791
1026
 
1027
+ // Register Matter RVC accessory if Matter is enabled
1028
+ if (this.matterEnabled) {
1029
+ try {
1030
+ const matterUUID = this.api.matter.uuid.generate(device.did)
1031
+
1032
+ // Check if this Matter accessory already exists
1033
+ const existingMatterAccessory = matterDevicesInHB.get(matterUUID)
1034
+
1035
+ if (!existingMatterAccessory) {
1036
+ // Create and publish new Matter RVC accessory
1037
+ const matterAccessory = this.createMatterRVCAccessory(device, accessory)
1038
+
1039
+ // Mark Matter accessory as not ready yet
1040
+ accessory.matterReady = false
1041
+ accessory.matterUUID = matterUUID
1042
+
1043
+ // Publish the accessory and listen for the READY event
1044
+ const published = this.api.matter.publishExternalAccessories(plugin.name, [matterAccessory])
1045
+
1046
+ // Listen for when the Matter accessory is ready
1047
+ if (published && published[0] && published[0]._eventEmitter) {
1048
+ published[0]._eventEmitter.on('ready', () => {
1049
+ accessory.matterReady = true
1050
+ accessory.log('Matter RVC accessory is ready.')
1051
+
1052
+ // Update initial state now that it's ready
1053
+ this.updateMatterStateFromHAP(accessory)
1054
+ })
1055
+ }
1056
+
1057
+ matterDevicesInHB.set(matterUUID, matterAccessory)
1058
+ this.log('[%s] Matter RVC accessory published.', accessory.displayName)
1059
+ } else {
1060
+ // Matter accessory already exists, just link it
1061
+ accessory.matterUUID = matterUUID
1062
+ accessory.matterReady = true // Assume existing accessories are ready
1063
+ accessory.logDebug('Matter RVC accessory already registered.')
1064
+ }
1065
+ } catch (err) {
1066
+ accessory.logWarn(`Failed to register Matter RVC accessory: ${parseError(err)}`)
1067
+ }
1068
+ }
1069
+
792
1070
  // Log configuration and device initialisation
793
1071
  this.log(
794
1072
  '[%s] %s: %s.',
@@ -912,31 +1190,38 @@ export default class {
912
1190
  }
913
1191
 
914
1192
  configureAccessory(accessory) {
915
- // Add the configured accessory to our global map
1193
+ this.log.debug(`[${accessory.displayName}] loading cached hap accessory.`)
916
1194
  devicesInHB.set(accessory.UUID, accessory)
917
- accessory
918
- .getService('Clean')
919
- .getCharacteristic(this.api.hap.Characteristic.On)
920
- .onSet(() => {
921
- this.log.warn('[%s] %s.', accessory.displayName, platformLang.accNotReady)
922
- throw new this.api.hap.HapStatusError(-70402)
923
- })
924
- .updateValue(new this.api.hap.HapStatusError(-70402))
925
- accessory
926
- .getService('Go Charge')
927
- .getCharacteristic(this.api.hap.Characteristic.On)
928
- .onSet(() => {
929
- this.log.warn('[%s] %s.', accessory.displayName, platformLang.accNotReady)
930
- throw new this.api.hap.HapStatusError(-70402)
931
- })
932
- .updateValue(new this.api.hap.HapStatusError(-70402))
1195
+ }
1196
+
1197
+ configureMatterAccessory(accessory) {
1198
+ this.log.debug(`[${accessory.displayName}] loading cached matter accessory.`)
1199
+ matterDevicesInHB.set(accessory.uuid, accessory)
933
1200
  }
934
1201
 
935
1202
  removeAccessory(accessory) {
936
1203
  // Remove an accessory from Homebridge
937
1204
  try {
1205
+ // Remove HAP accessory
938
1206
  this.api.unregisterPlatformAccessories(plugin.name, plugin.alias, [accessory])
939
1207
  devicesInHB.delete(accessory.UUID)
1208
+
1209
+ // Remove corresponding Matter accessory if it exists
1210
+ if (this.matterEnabled && accessory.context.ecoDeviceId) {
1211
+ try {
1212
+ const matterUUID = this.api.matter.uuid.generate(accessory.context.ecoDeviceId)
1213
+ const matterAccessory = matterDevicesInHB.get(matterUUID)
1214
+
1215
+ if (matterAccessory) {
1216
+ this.api.matter.unpublishExternalAccessories([matterAccessory])
1217
+ matterDevicesInHB.delete(matterUUID)
1218
+ this.log.debug('[%s] Matter RVC accessory removed.', accessory.displayName)
1219
+ }
1220
+ } catch (matterErr) {
1221
+ this.log.warn('[%s] Failed to remove Matter accessory: %s', accessory.displayName, parseError(matterErr))
1222
+ }
1223
+ }
1224
+
940
1225
  this.log('[%s] %s.', accessory.displayName, platformLang.devRemove)
941
1226
  } catch (err) {
942
1227
  // Catch any errors during remove
@@ -964,7 +1249,7 @@ export default class {
964
1249
  const order = value ? `Clean${accessory.context.commandSuffix}` : 'Stop'
965
1250
 
966
1251
  // Log the update
967
- accessory.log(`${platformLang.curCleaning} [${value ? platformLang.cleaning : platformLang.stop}}]`)
1252
+ accessory.log(`${platformLang.curCleaning} [${value ? platformLang.cleaning : platformLang.stop}]`)
968
1253
 
969
1254
  // Send the command
970
1255
  accessory.logDebug(`${platformLang.sendCmd} [${order}]`)
@@ -1255,15 +1540,25 @@ export default class {
1255
1540
 
1256
1541
  // Check if the new cleaning state is different from the cached state
1257
1542
  if (accessory.context.cacheClean !== newVal) {
1258
- // State is different so update service
1543
+ const isActive = ['auto', 'clean', 'edge', 'spot', 'spotarea', 'customarea'].includes(
1544
+ newVal.toLowerCase().replace(/[^a-z]+/g, ''),
1545
+ )
1546
+
1547
+ // State is different so update HAP service
1259
1548
  accessory
1260
1549
  .getService('Clean')
1261
- .updateCharacteristic(
1262
- this.hapChar.On,
1263
- ['auto', 'clean', 'edge', 'spot', 'spotarea', 'customarea'].includes(
1264
- newVal.toLowerCase().replace(/[^a-z]+/g, ''),
1265
- ),
1266
- )
1550
+ .updateCharacteristic(this.hapChar.On, isActive)
1551
+
1552
+ // Update Matter states
1553
+ if (isActive) {
1554
+ // Vacuum is cleaning
1555
+ this.updateMatterRunMode(accessory, 1) // Cleaning mode
1556
+ this.updateMatterOperationalState(accessory, 1) // Running state
1557
+ } else {
1558
+ // Vacuum is not cleaning - could be idle or stopped
1559
+ this.updateMatterRunMode(accessory, 0) // Idle mode
1560
+ this.updateMatterOperationalState(accessory, 0) // Stopped state
1561
+ }
1267
1562
 
1268
1563
  // Log the change
1269
1564
  accessory.log(`${platformLang.curCleaning} [${newVal}]`)
@@ -1356,7 +1651,7 @@ export default class {
1356
1651
 
1357
1652
  // Check if the new charging state is different from the cached state
1358
1653
  if (accessory.context.cacheCharge !== newVal) {
1359
- // State is different so update service
1654
+ // State is different so update HAP service
1360
1655
  accessory
1361
1656
  .getService('Go Charge')
1362
1657
  .updateCharacteristic(this.hapChar.On, newVal === 'returning')
@@ -1365,6 +1660,19 @@ export default class {
1365
1660
  .getService(this.hapServ.Battery)
1366
1661
  .updateCharacteristic(this.hapChar.ChargingState, chargeState)
1367
1662
 
1663
+ // Update Matter operational state based on charge state
1664
+ const chargeStateLower = newVal.toLowerCase()
1665
+ if (chargeStateLower === 'charging') {
1666
+ this.updateMatterRunMode(accessory, 0) // Idle mode
1667
+ this.updateMatterOperationalState(accessory, 65) // Charging
1668
+ } else if (chargeStateLower === 'returning') {
1669
+ this.updateMatterRunMode(accessory, 0) // Idle mode
1670
+ this.updateMatterOperationalState(accessory, 64) // Seeking Charger
1671
+ } else if (chargeStateLower === 'idle') {
1672
+ this.updateMatterRunMode(accessory, 0) // Idle mode
1673
+ this.updateMatterOperationalState(accessory, 66) // Docked
1674
+ }
1675
+
1368
1676
  // Log the change
1369
1677
  accessory.log(`${platformLang.curCharging} [${newVal}]`)
1370
1678
  }
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "displayName": "Homebridge Ecovacs",
4
4
  "alias": "Deebot",
5
5
  "type": "module",
6
- "version": "7.2.2",
6
+ "version": "7.2.4-beta.0",
7
7
  "description": "Homebridge plugin to integrate Ecovacs Deebot devices into HomeKit.",
8
8
  "author": {
9
9
  "name": "bwp91",
@@ -63,15 +63,15 @@
63
63
  },
64
64
  "dependencies": {
65
65
  "@homebridge/plugin-ui-utils": "^2.1.0",
66
- "ecovacs-deebot": "^0.9.6-beta.8",
67
- "patch-package": "^8.0.0"
66
+ "ecovacs-deebot": "^0.9.6-beta.12",
67
+ "patch-package": "^8.0.1"
68
68
  },
69
69
  "devDependencies": {
70
- "@antfu/eslint-config": "^4.17.0"
70
+ "@antfu/eslint-config": "^6.1.0"
71
71
  },
72
72
  "overrides": {
73
- "ecovacs-deebot": {
74
- "axios": "^1.10.0"
73
+ "request": {
74
+ "form-data": "^2.5.4"
75
75
  }
76
76
  }
77
77
  }
@@ -1,5 +1,5 @@
1
1
  diff --git a/node_modules/ecovacs-deebot/library/tools.js b/node_modules/ecovacs-deebot/library/tools.js
2
- index d484ba1..1ad1f14 100644
2
+ index d484ba1..eb8e866 100644
3
3
  --- a/node_modules/ecovacs-deebot/library/tools.js
4
4
  +++ b/node_modules/ecovacs-deebot/library/tools.js
5
5
  @@ -369,11 +369,13 @@ function envLogRaw(message) {