@homebridge-plugins/homebridge-ecovacs 7.0.2 → 7.1.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,11 +2,53 @@
2
2
 
3
3
  All notable changes to homebridge-ecovacs will be documented in this file.
4
4
 
5
- This project tries to adhere to [Semantic Versioning](http://semver.org/). In practice, this means that the version number will be incremented based on the following:
5
+ ## v7.2.4 (Unreleased)
6
6
 
7
- - `MAJOR` version when a minimum supported version of `homebridge` or `node` is increased to a new major version, or when a breaking change is made to the plugin config
8
- - `MINOR` version when a new device type is added, or when a new feature is added that is backwards-compatible
9
- - `PATCH` version when backwards-compatible bug fixes are implemented
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
+
18
+ ## v7.2.2 (2025-07-18)
19
+
20
+ ### Other Changes
21
+
22
+ - add maintainer message
23
+
24
+ ## v7.2.1 (2025-07-16)
25
+
26
+ ### Other Changes
27
+
28
+ - clear cache values on reset, fix go charge on
29
+
30
+ ## v7.2.0 (2025-07-13)
31
+
32
+ ### Notable Changes
33
+
34
+ - fix custom plugin config modal styles in ui 5
35
+ - fix custom characteristics for hb 2
36
+
37
+ ### Other Changes
38
+
39
+ - add `lint` step to build workflow
40
+ - add permissions to workflows
41
+ - improvements to the deprecate workflow
42
+
43
+ ## v7.1.0 (2025-07-12)
44
+
45
+ ### Notable Changes
46
+
47
+ - set `strictValidation` to `true` in the config schema file
48
+
49
+ ### Other Changes
50
+
51
+ - github repo maintenance
10
52
 
11
53
  ## v7.0.2 (2025-07-11)
12
54
 
@@ -296,7 +338,7 @@ This project tries to adhere to [Semantic Versioning](http://semver.org/). In pr
296
338
 
297
339
  - Ignore `Robot is operational` error in log
298
340
  - Updated `ecovacs-deebot` library to v0.6.3
299
- - Remove `node-machine-id` in favour of generating a client id based on ECOVACS username
341
+ - Remove `node-machine-id` in favour of generating a client id based on Ecovacs username
300
342
 
301
343
  ## v4.1.0 (2021-08-30)
302
344
 
@@ -338,7 +380,7 @@ _Unpublished_
338
380
 
339
381
  ### Added
340
382
 
341
- - Support for cleaning 'Spot Areas' customised in the ECOVACS app
383
+ - Support for cleaning 'Spot Areas' customised in the Ecovacs app
342
384
 
343
385
  ## v3.3.1 (2021-07-18)
344
386
 
@@ -442,7 +484,7 @@ _Unpublished_
442
484
 
443
485
  - Configuration settings per Deebot device
444
486
  - Support for Chinese server login
445
- - Enter your ECOVACS password as a base64 encoded string and use the option `encodedPassword` to let the plugin know
487
+ - Enter your Ecovacs password as a base64 encoded string and use the option `encodedPassword` to let the plugin know
446
488
  - More viewable information in the Homebridge plugin-ui:
447
489
  - Device model, company and an image of your device in case you didn't know what it looked like
448
490
 
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
 
6
6
  # homebridge-ecovacs
7
7
 
8
- Homebridge plugin to integrate ECOVACS Deebot or Yeedi devices into HomeKit
8
+ Homebridge plugin to integrate Ecovacs Deebot or Yeedi devices into HomeKit
9
9
 
10
10
  [![npm](https://img.shields.io/npm/v/@homebridge-plugins/homebridge-ecovacs/latest?label=latest)](https://www.npmjs.com/package/@homebridge-plugins/homebridge-ecovacs)
11
11
  [![npm](https://img.shields.io/npm/v/@homebridge-plugins/homebridge-ecovacs/beta?label=beta)](https://github.com/homebridge-plugins/homebridge-ecovacs/wiki/Beta-Version)
@@ -20,10 +20,12 @@ Homebridge plugin to integrate ECOVACS Deebot or Yeedi devices into HomeKit
20
20
 
21
21
  ### Plugin Information
22
22
 
23
- - This plugin allows you to view and control your ECOVACS or Yeedi devices within HomeKit. The plugin:
24
- - requires your ECOVACS or Yeedi credentials to function
23
+ - This plugin allows you to view and control your Ecovacs or Yeedi devices within HomeKit. The plugin:
24
+ - requires your Ecovacs or Yeedi credentials to function
25
25
  - uses a cloud-based connection (i.e. requires the internet)
26
26
 
27
+ > I'm looking for some lovely people to help maintain this plugin, please get in touch on GitHub or Discord if you'd like to help out 😄
28
+
27
29
  ### Prerequisites
28
30
 
29
31
  - To use this plugin, you will need to already have:
@@ -59,5 +61,5 @@ Homebridge plugin to integrate ECOVACS Deebot or Yeedi devices into HomeKit
59
61
 
60
62
  ### Disclaimer
61
63
 
62
- - I am in no way affiliated with ECOVACS/Deebot/Yeedi and this plugin is a personal project that I maintain in my free time.
64
+ - I am in no way affiliated with Ecovacs/Deebot/Yeedi and this plugin is a personal project that I maintain in my free time.
63
65
  - Use this plugin entirely at your own risk - please see licence for more information.
@@ -2,6 +2,7 @@
2
2
  "pluginAlias": "Deebot",
3
3
  "pluginType": "platform",
4
4
  "singular": true,
5
+ "strictValidation": true,
5
6
  "customUi": true,
6
7
  "customUiPath": "./lib/homebridge-ui",
7
8
  "headerDisplay": "<p align=\"center\"><img width=\"60%\" src=\"https://user-images.githubusercontent.com/43026681/101321841-f0eb5280-385d-11eb-8dd4-f57113f6e078.png\"></p><p align=\"center\">For help and support please visit our <a href=\"https://github.com/homebridge-plugins/homebridge-ecovacs/wiki\">GitHub Wiki</a>. We hope you find this plugin useful!</p>",
@@ -16,7 +17,7 @@
16
17
  "useYeedi": {
17
18
  "title": "Use Yeedi Login",
18
19
  "type": "boolean",
19
- "description": "Enable this for Yeedi login instead of ECOVACS."
20
+ "description": "Enable this for Yeedi login instead of Ecovacs."
20
21
  },
21
22
  "countryCode": {
22
23
  "title": "Country Code",
@@ -25,13 +26,13 @@
25
26
  "required": true
26
27
  },
27
28
  "username": {
28
- "title": "ECOVACS/Yeedi Username",
29
+ "title": "Ecovacs/Yeedi Username",
29
30
  "required": true,
30
31
  "type": "string",
31
- "description": "Your username, if your account is based in China you should use your ECOVACS ID."
32
+ "description": "Your username, if your account is based in China you should use your Ecovacs ID."
32
33
  },
33
34
  "password": {
34
- "title": "ECOVACS/Yeedi Password",
35
+ "title": "Ecovacs/Yeedi Password",
35
36
  "required": true,
36
37
  "type": "string",
37
38
  "description": "Your password, can also be a base64 encoded version of your password."
@@ -56,7 +57,7 @@
56
57
  "deviceId": {
57
58
  "title": "Device ID",
58
59
  "type": "string",
59
- "description": "ECOVACS Device ID of the device these settings apply to, can be of format E2000000000000000000 or 11111111-aaaa-bbbb-2222-cccccccccccc."
60
+ "description": "Ecovacs Device ID of the device these settings apply to, can be of format E2000000000000000000 or 11111111-aaaa-bbbb-2222-cccccccccccc."
60
61
  },
61
62
  "ignoreDevice": {
62
63
  "type": "boolean",
@@ -70,7 +71,7 @@
70
71
  "title": "Polling Interval",
71
72
  "type": "integer",
72
73
  "placeholder": 120,
73
- "description": "An interval (in seconds) in which this device will refresh with ECOVACS. Set to 0 to disable, otherwise must be 30 or more."
74
+ "description": "An interval (in seconds) in which this device will refresh with Ecovacs. Set to 0 to disable, otherwise must be 30 or more."
74
75
  },
75
76
  "hideMotionSensor": {
76
77
  "title": "Hide Motion Sensor",
@@ -1,3 +1,39 @@
1
+ <style>
2
+ .dark-mode {
3
+ background-color: #242424;
4
+ color: lightgrey;
5
+
6
+ .form-control {
7
+ background-color: #333333 !important;
8
+ border: none !important;
9
+ color: #eeeeee !important;
10
+ }
11
+
12
+ thead, tbody, tr {
13
+ border-style: hidden;
14
+ }
15
+
16
+ th {
17
+ color: #eeeeee !important;
18
+ font-weight: 500 !important;
19
+ }
20
+ }
21
+ select {
22
+ background-image:
23
+ linear-gradient(45deg, transparent 50%, gray 50%),
24
+ linear-gradient(135deg, gray 50%, transparent 50%),
25
+ linear-gradient(to right, #ccc, #ccc);
26
+ background-position:
27
+ calc(100% - 20px) calc(1em + 2px),
28
+ calc(100% - 15px) calc(1em + 2px),
29
+ calc(100% - 2.5em) 0.5em;
30
+ background-size:
31
+ 5px 5px,
32
+ 5px 5px,
33
+ 1px 1.5em;
34
+ background-repeat: no-repeat;
35
+ }
36
+ </style>
1
37
  <p class="text-center">
2
38
  <img
3
39
  src="https://user-images.githubusercontent.com/43026681/101321841-f0eb5280-385d-11eb-8dd4-f57113f6e078.png"
@@ -7,7 +43,7 @@
7
43
  </p>
8
44
  <div id="pageIntro" class="text-center" style="display: none;">
9
45
  <p class="lead">Thank you for installing <strong>homebridge-ecovacs</strong></p>
10
- <p>You will need to enter your ECOVACS username, password and region on the next page</p>
46
+ <p>You will need to enter your Ecovacs username, password and region on the next page</p>
11
47
  <button type="button" class="btn btn-primary" id="introContinue">Continue &rarr;</button>
12
48
  </div>
13
49
  <div
@@ -28,7 +64,7 @@
28
64
  <select class="form-control" id="deviceSelect"></select>
29
65
  </div>
30
66
  </form>
31
- <table class="table w-100" id="deviceTable" style="display: none;">
67
+ <table class="table w-100 mt-3" id="deviceTable" style="display: none;">
32
68
  <thead>
33
69
  <tr class="table-active">
34
70
  <th scope="col" style="width: 40%;">Device Name</th>
@@ -161,7 +197,7 @@
161
197
  <h4>Disclaimer</h4>
162
198
  <ul>
163
199
  <li>
164
- I am in no way affiliated with ECOVACS and this plugin is a personal project that I maintain
200
+ I am in no way affiliated with Ecovacs and this plugin is a personal project that I maintain
165
201
  in my free time.
166
202
  </li>
167
203
  <li>Use this plugin entirely at your own risk - please see licence for more information.</li>
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 = []
@@ -363,7 +367,7 @@ export default class {
363
367
  // Require any libraries that the accessory instances use
364
368
  this.cusChar = new platformChars(this.api)
365
369
 
366
- // Connect to ECOVACS/Yeedi
370
+ // Connect to Ecovacs/Yeedi
367
371
  this.ecovacsAPI = new EcoVacsAPI(
368
372
  EcoVacsAPI.getDeviceId(this.api.hap.uuid.generate(this.config.username)),
369
373
  this.config.countryCode,
@@ -374,7 +378,14 @@ 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
 
377
- // Attempt to log in to ECOVACS/Yeedi
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
+
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))
380
391
  } catch (err) {
@@ -393,7 +404,7 @@ export default class {
393
404
  }
394
405
  }
395
406
 
396
- // Get a device list from ECOVACS/Yeedi
407
+ // Get a device list from Ecovacs/Yeedi
397
408
  const deviceList = await this.ecovacsAPI.devices()
398
409
 
399
410
  // Check the request for device list was successful
@@ -402,7 +413,7 @@ export default class {
402
413
  }
403
414
 
404
415
  // Initialise each device into Homebridge
405
- this.log('[%s] %s.', deviceList.length, platformLang.deviceCount(this.config.useYeedi ? 'Yeedi' : 'ECOVACS'))
416
+ this.log('[%s] %s.', deviceList.length, platformLang.deviceCount(this.config.useYeedi ? 'Yeedi' : 'Ecovacs'))
406
417
  for (let i = 0; i < deviceList.length; i += 1) {
407
418
  await this.initialiseDevice(deviceList[i])
408
419
  }
@@ -429,7 +440,7 @@ export default class {
429
440
  clearInterval(this.refreshIntervals[id])
430
441
  })
431
442
 
432
- // Disconnect from each ECOVACS/Yeedi device
443
+ // Disconnect from each Ecovacs/Yeedi device
433
444
  devicesInHB.forEach((accessory) => {
434
445
  if (accessory.control?.is_ready) {
435
446
  accessory.control.disconnect()
@@ -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
@@ -453,7 +688,7 @@ export default class {
453
688
  return
454
689
  }
455
690
 
456
- // Load the device control information from ECOVACS/Yeedi
691
+ // Load the device control information from Ecovacs/Yeedi
457
692
  const loadedDevice = this.ecovacsAPI.getVacBot(
458
693
  this.ecovacsAPI.uid,
459
694
  EcoVacsAPI.REALM,
@@ -495,6 +730,21 @@ export default class {
495
730
  accessory.context.isOnline = false
496
731
  accessory.context.lastMsg = ''
497
732
 
733
+ // Remove the existing cache values from the context
734
+ // accessory.context.cacheClean
735
+ // accessory.context.cacheCharge
736
+ // accessory.context.cacheAirDrying
737
+ // accessory.context.cacheSpeed
738
+ // accessory.context.cacheBattery
739
+ const cacheKeys = [
740
+ 'cacheClean',
741
+ 'cacheCharge',
742
+ 'cacheAirDrying',
743
+ 'cacheSpeed',
744
+ 'cacheBattery',
745
+ ]
746
+ cacheKeys.forEach(key => delete accessory.context[key])
747
+
498
748
  // Add the 'clean' switch service if it doesn't already exist
499
749
  const cleanService = accessory.getService('Clean') || accessory.addService(this.hapServ.Switch, 'Clean', 'clean')
500
750
  if (!cleanService.testCharacteristic(this.hapChar.ConfiguredName)) {
@@ -774,6 +1024,49 @@ export default class {
774
1024
  this.api.updatePlatformAccessories([accessory])
775
1025
  devicesInHB.set(accessory.UUID, accessory)
776
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
+
777
1070
  // Log configuration and device initialisation
778
1071
  this.log(
779
1072
  '[%s] %s: %s.',
@@ -897,31 +1190,38 @@ export default class {
897
1190
  }
898
1191
 
899
1192
  configureAccessory(accessory) {
900
- // Add the configured accessory to our global map
1193
+ this.log.debug(`[${accessory.displayName}] loading cached hap accessory.`)
901
1194
  devicesInHB.set(accessory.UUID, accessory)
902
- accessory
903
- .getService('Clean')
904
- .getCharacteristic(this.api.hap.Characteristic.On)
905
- .onSet(() => {
906
- this.log.warn('[%s] %s.', accessory.displayName, platformLang.accNotReady)
907
- throw new this.api.hap.HapStatusError(-70402)
908
- })
909
- .updateValue(new this.api.hap.HapStatusError(-70402))
910
- accessory
911
- .getService('Go Charge')
912
- .getCharacteristic(this.api.hap.Characteristic.On)
913
- .onSet(() => {
914
- this.log.warn('[%s] %s.', accessory.displayName, platformLang.accNotReady)
915
- throw new this.api.hap.HapStatusError(-70402)
916
- })
917
- .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)
918
1200
  }
919
1201
 
920
1202
  removeAccessory(accessory) {
921
1203
  // Remove an accessory from Homebridge
922
1204
  try {
1205
+ // Remove HAP accessory
923
1206
  this.api.unregisterPlatformAccessories(plugin.name, plugin.alias, [accessory])
924
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
+
925
1225
  this.log('[%s] %s.', accessory.displayName, platformLang.devRemove)
926
1226
  } catch (err) {
927
1227
  // Catch any errors during remove
@@ -949,7 +1249,7 @@ export default class {
949
1249
  const order = value ? `Clean${accessory.context.commandSuffix}` : 'Stop'
950
1250
 
951
1251
  // Log the update
952
- accessory.log(`${platformLang.curCleaning} [${value ? platformLang.cleaning : platformLang.stop}}]`)
1252
+ accessory.log(`${platformLang.curCleaning} [${value ? platformLang.cleaning : platformLang.stop}]`)
953
1253
 
954
1254
  // Send the command
955
1255
  accessory.logDebug(`${platformLang.sendCmd} [${order}]`)
@@ -1023,7 +1323,7 @@ export default class {
1023
1323
  // Avoid quick switching with this function
1024
1324
  const updateKey = Math.random()
1025
1325
  .toString(36)
1026
- .substr(2, 8)
1326
+ .slice(2, 10)
1027
1327
  accessory.context.lastCommandKey = updateKey
1028
1328
  await sleep(1)
1029
1329
  if (updateKey !== accessory.context.lastCommandKey) {
@@ -1135,7 +1435,7 @@ export default class {
1135
1435
  setTimeout(() => {
1136
1436
  accessory
1137
1437
  .getService('Go Charge')
1138
- .updateCharacteristic(this.hapChar.On, accessory.context.cacheCharge === 'charging')
1438
+ .updateCharacteristic(this.hapChar.On, accessory.context.cacheCharge === 'returning')
1139
1439
  }, 2000)
1140
1440
  throw new this.hapErr(-70402)
1141
1441
  }
@@ -1240,15 +1540,25 @@ export default class {
1240
1540
 
1241
1541
  // Check if the new cleaning state is different from the cached state
1242
1542
  if (accessory.context.cacheClean !== newVal) {
1243
- // 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
1244
1548
  accessory
1245
1549
  .getService('Clean')
1246
- .updateCharacteristic(
1247
- this.hapChar.On,
1248
- ['auto', 'clean', 'edge', 'spot', 'spotarea', 'customarea'].includes(
1249
- newVal.toLowerCase().replace(/[^a-z]+/g, ''),
1250
- ),
1251
- )
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
+ }
1252
1562
 
1253
1563
  // Log the change
1254
1564
  accessory.log(`${platformLang.curCleaning} [${newVal}]`)
@@ -1341,7 +1651,7 @@ export default class {
1341
1651
 
1342
1652
  // Check if the new charging state is different from the cached state
1343
1653
  if (accessory.context.cacheCharge !== newVal) {
1344
- // State is different so update service
1654
+ // State is different so update HAP service
1345
1655
  accessory
1346
1656
  .getService('Go Charge')
1347
1657
  .updateCharacteristic(this.hapChar.On, newVal === 'returning')
@@ -1350,6 +1660,19 @@ export default class {
1350
1660
  .getService(this.hapServ.Battery)
1351
1661
  .updateCharacteristic(this.hapChar.ChargingState, chargeState)
1352
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
+
1353
1676
  // Log the change
1354
1677
  accessory.log(`${platformLang.curCharging} [${newVal}]`)
1355
1678
  }
@@ -1,50 +1,51 @@
1
- import { inherits } from 'node:util'
2
-
3
1
  export default class {
4
2
  constructor(api) {
5
- this.hapChar = api.hap.Characteristic
6
3
  this.uuids = {
7
4
  maxSpeed: 'E963F001-079E-48FF-8F27-9C2605A29F52',
8
5
  predefinedArea: 'E963F002-079E-48FF-8F27-9C2605A29F52',
9
6
  trueDetect: 'E963F003-079E-48FF-8F27-9C2605A29F52',
10
7
  }
11
- const self = this
12
- this.MaxSpeed = function MaxSpeed() {
13
- self.hapChar.call(this, 'Max Speed', self.uuids.maxSpeed)
14
- this.setProps({
15
- format: api.hap.Formats.BOOL,
16
- perms: [api.hap.Perms.PAIRED_READ, api.hap.Perms.PAIRED_WRITE, api.hap.Perms.NOTIFY],
17
- })
18
- this.value = this.getDefaultValue()
8
+ const uuids = this.uuids
9
+
10
+ this.MaxSpeed = class extends api.hap.Characteristic {
11
+ constructor() {
12
+ super('Max Speed', uuids.maxSpeed)
13
+ this.setProps({
14
+ format: api.hap.Formats.BOOL,
15
+ perms: [api.hap.Perms.PAIRED_READ, api.hap.Perms.PAIRED_WRITE, api.hap.Perms.NOTIFY],
16
+ })
17
+ this.value = this.getDefaultValue()
18
+ }
19
19
  }
20
20
 
21
- this.PredefinedArea = function PredefinedArea() {
22
- self.hapChar.call(this, 'Predefined Area', self.uuids.predefinedArea)
23
- this.setProps({
24
- format: api.hap.Formats.UINT8,
25
- perms: [api.hap.Perms.PAIRED_READ, api.hap.Perms.PAIRED_WRITE, api.hap.Perms.NOTIFY],
26
- minValue: 0,
27
- maxValue: 15,
28
- minStep: 1,
29
- validValues: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
30
- })
31
- this.value = this.getDefaultValue()
21
+ this.PredefinedArea = class extends api.hap.Characteristic {
22
+ constructor() {
23
+ super('Predefined Area', uuids.predefinedArea)
24
+ this.setProps({
25
+ format: api.hap.Formats.UINT8,
26
+ perms: [api.hap.Perms.PAIRED_READ, api.hap.Perms.PAIRED_WRITE, api.hap.Perms.NOTIFY],
27
+ minValue: 0,
28
+ maxValue: 15,
29
+ minStep: 1,
30
+ validValues: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
31
+ })
32
+ this.value = this.getDefaultValue()
33
+ }
32
34
  }
33
35
 
34
- this.TrueDetect = function TrueDetect() {
35
- self.hapChar.call(this, 'TrueDetect', self.uuids.trueDetect)
36
- this.setProps({
37
- format: api.hap.Formats.BOOL,
38
- perms: [api.hap.Perms.PAIRED_READ, api.hap.Perms.PAIRED_WRITE, api.hap.Perms.NOTIFY],
39
- })
40
- this.value = this.getDefaultValue()
36
+ this.TrueDetect = class extends api.hap.Characteristic {
37
+ constructor() {
38
+ super('TrueDetect', uuids.trueDetect)
39
+ this.setProps({
40
+ format: api.hap.Formats.BOOL,
41
+ perms: [api.hap.Perms.PAIRED_READ, api.hap.Perms.PAIRED_WRITE, api.hap.Perms.NOTIFY],
42
+ })
43
+ this.value = this.getDefaultValue()
44
+ }
41
45
  }
42
46
 
43
- inherits(this.MaxSpeed, this.hapChar)
44
- inherits(this.PredefinedArea, this.hapChar)
45
- inherits(this.TrueDetect, this.hapChar)
46
- this.MaxSpeed.UUID = this.uuids.maxSpeed
47
47
  this.PredefinedArea.UUID = this.uuids.predefinedArea
48
48
  this.TrueDetect.UUID = this.uuids.trueDetect
49
+ this.MaxSpeed.UUID = this.uuids.maxSpeed
49
50
  }
50
51
  }
@@ -74,6 +74,6 @@ export default {
74
74
  speedFail: 'sending speed update failed as',
75
75
  stop: 'stop',
76
76
  typeForArea: 'Type for predefined area',
77
- welcome: 'This plugin has been made with by bwp91, please consider a on GitHub if you are finding it useful!',
77
+ welcome: 'I\'m looking for some lovely people to help maintain this plugin, please get in touch on GitHub or Discord if you\'d like to help out 😄',
78
78
  unknownCommandTypeForArea: 'Unknown command type for predefined area',
79
79
  }
package/package.json CHANGED
@@ -3,8 +3,8 @@
3
3
  "displayName": "Homebridge Ecovacs",
4
4
  "alias": "Deebot",
5
5
  "type": "module",
6
- "version": "7.0.2",
7
- "description": "Homebridge plugin to integrate ECOVACS Deebot devices into HomeKit.",
6
+ "version": "7.1.4-beta.0",
7
+ "description": "Homebridge plugin to integrate Ecovacs Deebot devices into HomeKit.",
8
8
  "author": {
9
9
  "name": "bwp91",
10
10
  "email": "bwp91@icloud.com"
@@ -57,21 +57,21 @@
57
57
  "node": "^20 || ^22 || ^24"
58
58
  },
59
59
  "scripts": {
60
- "lint": "eslint . --fix",
61
- "postinstall": "patch-package",
62
- "rebuild": "rm -rf package-lock.json && rm -rf node_modules && npm install"
60
+ "lint": "eslint . --max-warnings=0",
61
+ "lint:fix": "npm run lint -- --fix",
62
+ "postinstall": "patch-package"
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.16.2"
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) {