homebridge-midea-platform 0.4.13 → 1.0.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.
Files changed (73) hide show
  1. package/.gitattributes +3 -0
  2. package/.husky/pre-commit +2 -1
  3. package/.prettierrc +1 -0
  4. package/README.md +14 -156
  5. package/config.schema.json +162 -1
  6. package/dist/accessory/AccessoryFactory.d.ts +3 -1
  7. package/dist/accessory/AccessoryFactory.d.ts.map +1 -1
  8. package/dist/accessory/AccessoryFactory.js +6 -0
  9. package/dist/accessory/AccessoryFactory.js.map +1 -1
  10. package/dist/accessory/AirConditionerAccessory.d.ts +9 -0
  11. package/dist/accessory/AirConditionerAccessory.d.ts.map +1 -1
  12. package/dist/accessory/AirConditionerAccessory.js +88 -12
  13. package/dist/accessory/AirConditionerAccessory.js.map +1 -1
  14. package/dist/accessory/DehumidifierAccessory.d.ts +5 -0
  15. package/dist/accessory/DehumidifierAccessory.d.ts.map +1 -1
  16. package/dist/accessory/DehumidifierAccessory.js +42 -2
  17. package/dist/accessory/DehumidifierAccessory.js.map +1 -1
  18. package/dist/accessory/ElectricWaterHeaterAccessory.d.ts +45 -0
  19. package/dist/accessory/ElectricWaterHeaterAccessory.d.ts.map +1 -0
  20. package/dist/accessory/ElectricWaterHeaterAccessory.js +179 -0
  21. package/dist/accessory/ElectricWaterHeaterAccessory.js.map +1 -0
  22. package/dist/accessory/GasWaterHeaterAccessory.d.ts +52 -0
  23. package/dist/accessory/GasWaterHeaterAccessory.d.ts.map +1 -0
  24. package/dist/accessory/GasWaterHeaterAccessory.js +219 -0
  25. package/dist/accessory/GasWaterHeaterAccessory.js.map +1 -0
  26. package/dist/core/MideaCloud.d.ts +14 -14
  27. package/dist/core/MideaCloud.d.ts.map +1 -1
  28. package/dist/core/MideaCloud.js +150 -92
  29. package/dist/core/MideaCloud.js.map +1 -1
  30. package/dist/core/MideaConstants.d.ts +7 -2
  31. package/dist/core/MideaConstants.d.ts.map +1 -1
  32. package/dist/core/MideaConstants.js +8 -1
  33. package/dist/core/MideaConstants.js.map +1 -1
  34. package/dist/core/MideaPacketBuilder.d.ts.map +1 -1
  35. package/dist/core/MideaPacketBuilder.js +3 -2
  36. package/dist/core/MideaPacketBuilder.js.map +1 -1
  37. package/dist/core/MideaSecurity.d.ts +35 -11
  38. package/dist/core/MideaSecurity.d.ts.map +1 -1
  39. package/dist/core/MideaSecurity.js +75 -28
  40. package/dist/core/MideaSecurity.js.map +1 -1
  41. package/dist/core/MideaUtils.js +2 -1
  42. package/dist/core/MideaUtils.js.map +1 -1
  43. package/dist/devices/DeviceFactory.d.ts +4 -2
  44. package/dist/devices/DeviceFactory.d.ts.map +1 -1
  45. package/dist/devices/DeviceFactory.js +6 -0
  46. package/dist/devices/DeviceFactory.js.map +1 -1
  47. package/dist/devices/e2/MideaE2Device.d.ts +45 -0
  48. package/dist/devices/e2/MideaE2Device.d.ts.map +1 -0
  49. package/dist/devices/e2/MideaE2Device.js +120 -0
  50. package/dist/devices/e2/MideaE2Device.js.map +1 -0
  51. package/dist/devices/e2/MideaE2Message.d.ts +34 -0
  52. package/dist/devices/e2/MideaE2Message.d.ts.map +1 -0
  53. package/dist/devices/e2/MideaE2Message.js +133 -0
  54. package/dist/devices/e2/MideaE2Message.js.map +1 -0
  55. package/dist/devices/e3/MideaE3Device.d.ts +44 -0
  56. package/dist/devices/e3/MideaE3Device.d.ts.map +1 -0
  57. package/dist/devices/e3/MideaE3Device.js +126 -0
  58. package/dist/devices/e3/MideaE3Device.js.map +1 -0
  59. package/dist/devices/e3/MideaE3Message.d.ts +52 -0
  60. package/dist/devices/e3/MideaE3Message.d.ts.map +1 -0
  61. package/dist/devices/e3/MideaE3Message.js +137 -0
  62. package/dist/devices/e3/MideaE3Message.js.map +1 -0
  63. package/dist/platformUtils.d.ts +26 -0
  64. package/dist/platformUtils.d.ts.map +1 -1
  65. package/dist/platformUtils.js +24 -0
  66. package/dist/platformUtils.js.map +1 -1
  67. package/docs/a1.md +20 -0
  68. package/docs/ac.md +41 -0
  69. package/docs/e2.md +21 -0
  70. package/docs/e3.md +27 -0
  71. package/homebridge-ui/public/index.html +78 -25
  72. package/homebridge-ui/server.js +23 -4
  73. package/package.json +8 -1
package/docs/a1.md ADDED
@@ -0,0 +1,20 @@
1
+ # Dehumidifier
2
+
3
+ Providing dehumidifier settings is optional and the whole section or individual options may be ommitted and default values (noted below) will be used. Within the *devices.config* object the following dehumidifier specific options.
4
+
5
+ ```json
6
+ "A1_options": {
7
+ "minHumidity": 35,
8
+ "maxHumidity": 85,
9
+ "humidityStep": 5,
10
+ "temperatureSensor": false,
11
+ "pumpSwitch": false
12
+ }
13
+ ```
14
+
15
+ ## Options
16
+ - **minHumidity** *(optional)*: The minimum relative humidity that the unit can be set for. Default is 35%
17
+ - **maxHumidity** *(optional)*: The maximum relative humidity that the unit can be set for. Default is 85%
18
+ - **humidityStep** *(optional)*: Increment in which the relative himidity setting can be changed, may be set to either 5% or 10%. The default is 5%.
19
+ - **temperatureSensor** *(optional)*: Toggles if the temperature sensor is created with the accessory. Default is false.
20
+ - **pumpSwitch** *(optional)*: Toggles if the pump switch is created with the accessory. Default is false.
package/docs/ac.md ADDED
@@ -0,0 +1,41 @@
1
+ # Air Conditioner
2
+
3
+ Providing air conditioner settings is optional and the whole section or individual options may be ommitted and default values (noted below) will be used. Within the *devices.config* object the following air conditioner specific options.
4
+
5
+ ```json
6
+ "AC_options": {
7
+ "swingMode": "Both",
8
+ "heatingCapable": false,
9
+ "ecoSwitch": false,
10
+ "displaySwitch": {
11
+ "flag": true,
12
+ "command": false
13
+ },
14
+ "minTemp": 16,
15
+ "maxTemp": 30,
16
+ "tempStep": 1,
17
+ "fahrenheit": false,
18
+ "fanOnlyMode": false,
19
+ "breezeAwaySwitch": false,
20
+ "dryModeSwitch": false,
21
+ "auxHeatingSwitches": false,
22
+ "outDoorTemp": false,
23
+ "audioFeedback": false
24
+ }
25
+ ```
26
+ ## Options
27
+ - **swingMode** *(optional)*: The swing mode of the unit. Options are "Both", "Vertical", "Horizontal", or "None". Default is "None".
28
+ - **heatingCapable** *(optional)*: Toggles if the unit is capable of heating. Default is false.
29
+ - **displaySwitch** *(optional)*: Object with following two options...
30
+ - * **flag** *(optional)*: Toggles if a switch, which can turn the display on or off will be created or not. Default is true.
31
+ - * **command** *(optional)*: Use this if the switch display command does not work. If it doesn't work either way then you unit does not support this feature. Default is false.
32
+ - **minTemp** *(optional)*: The minimum temperature that the unit can be set for. Default is 16 celsius
33
+ - **maxTemp** *(optional)*: The maximum temperature that the unit can be set for. Default is 30 celsius
34
+ - **tempStep** *(optional)*: Increment in which the temperature setting can be changed, may be set to either 0.5 or 1 degree celsius. The default is one degree.
35
+ - **fahrenheit** *(optional)*: Toggles if the temperature on the unit is displayed in Fahrenheit or Celsius. Default is false (displays in Celsius).
36
+ - **fanOnlyMode** *(optional)*: Toggles if the fan only mode is created with the accessory. Default is false.
37
+ - **breezeAwaySwitch** *(optional)*: Toggles if the breeze away switch is created with the accessory. Default is false.
38
+ - **dryModeSwitch** *(optional)*: Toggles if the dry mode switch is created with the accessory. Default is false.
39
+ - **auxHeatingSwitches** *(optional)*: Toggles if the aux heating switches are created with the accessory. Default is false.
40
+ - **outDoorTemp** *(optional)*: Toggles if the outdoor temperature sensor is created with the accessory. Default is false.
41
+ - **audioFeedback** *(optional)*: Toggles if the unit beeps when a command is sent, default is false.
package/docs/e2.md ADDED
@@ -0,0 +1,21 @@
1
+ # Electric Water Heater
2
+
3
+ Providing electric water heater settings is optional and the whole section or individual options may be ommitted and default values (noted below) will be used. Within the *devices.config* object the following electric water heater specific options.
4
+
5
+ ```json
6
+ "E2_options": {
7
+ "protocol": "auto",
8
+ "minTemp": 30,
9
+ "maxTemp": 75,
10
+ "tempStep": 1,
11
+ "variableHeatingSwitch": false,
12
+ "wholeTankHeatingSwitch": false,
13
+ }
14
+ ```
15
+ ## Options
16
+ - **protocol** *(optional)*: The protocol used to control the water heater. Only change this if the device is not responding to the commands. Default is "auto". Other options are "auto", "old", "new".
17
+ - **minTemp** *(optional)*: The minimum temperature that the unit can be set for. Default is 30 celsius
18
+ - **maxTemp** *(optional)*: The maximum temperature that the unit can be set for. Default is 75 celsius
19
+ - **tempStep** *(optional)*: Increment in which the temperature setting can be changed, may be set to either 0.5 or 1 degree celsius. The default is one degree.
20
+ - **variableHeatingSwitch** *(optional)*: Toggles if the variable heating switch is created with the accessory. Default is false.
21
+ - **wholeTankHeatingSwitch** *(optional)*: Toggles if the whole tank heating switch is created with the accessory. Default is false.
package/docs/e3.md ADDED
@@ -0,0 +1,27 @@
1
+ # Gas Water Heater
2
+
3
+ Providing gas water heater settings is optional and the whole section or individual options may be ommitted and default values (noted below) will be used. Within the *devices.config* object the following gas water heater specific options.
4
+
5
+ ```json
6
+ "E3_options": {
7
+ "precisionHalves": false,
8
+ "minTemp": 35,
9
+ "maxTemp": 65,
10
+ "tempStep": 1,
11
+ "burningStateSensor": false,
12
+ "protectionSensor": false,
13
+ "zeroColdWaterSwitch": false,
14
+ "zeroColdPulseSwitch": false,
15
+ "smartVolumeSwitch": false
16
+ }
17
+ ```
18
+ ## Options
19
+ - **precisionHalves** *(optional)*: This option must be enabled if the temperature on your unit is displayed in half degree increments. Default is false.
20
+ - **minTemp** *(optional)*: The minimum temperature that the unit can be set for. Default is 35 celsius
21
+ - **maxTemp** *(optional)*: The maximum temperature that the unit can be set for. Default is 65 celsius
22
+ - **tempStep** *(optional)*: Increment in which the temperature setting can be changed, may be set to either 0.5 or 1 degree celsius. The default is one degree.
23
+ - **burningStateSensor** *(optional)*: Toggles if the burning state sensor is created with the accessory. Default is false.
24
+ - **protectionSensor** *(optional)*: Toggles if the protection sensor is created with the accessory. Default is false.
25
+ - **zeroColdWaterSwitch** *(optional)*: Toggles if the zero cold water switch is created with the accessory. Default is false.
26
+ - **zeroColdPulseSwitch** *(optional)*: Toggles if the zero cold pulse switch is created with the accessory. Default is false.
27
+ - **smartVolumeSwitch** *(optional)*: Toggles if the smart volume switch is created with the accessory. Default is false.
@@ -26,25 +26,41 @@
26
26
  <div class="card-body">
27
27
  <form>
28
28
  <div class="mb-3 bg-info-subtle border-info-subtle p-3 rounded-2">
29
- You can discover your devices without providing credentials. However, if you see <i>No Credentials</i> in the
30
- Add/Update column then you must provide login credentials so that a <i>token/key</i> pair can be retrieved
31
- from Midea cloud servers, these are protocol version 3 devices.
29
+ You can discover your devices without providing credentials. However, if you see <i>No Credentials</i> in
30
+ the
31
+ Add/Update column then you must provide login credentials or use the default profile option so the
32
+ <i>token/key</i> pair can be retrieved
33
+ from Midea cloud servers.<br/>
34
+ <i>Note: you don't have to have your devices connected to the account you are using to fetch the token/key pair.</i>
32
35
  </div>
33
- <div class="mb-3">
34
- <label for="registeredApp" class="form-label">Registered app</label>
35
- <select id="registeredApp" class="form-select">
36
- <option selected value="Midea SmartHome (MSmartHome)">Midea SmartHome (MSmartHome)</option>
37
- <option value="NetHome Plus">NetHome Plus</option>
38
- <option value="Meiju">Meiju</option>
39
- </select>
36
+ <div class="mb-3 form-check form-switch">
37
+ <input id="useDefaultProfile" type="checkbox" class="form-check-input" role="switch">
38
+ <label for="useDefaultProfile" class="form-check-label">Use default Midea SmartHome profile</label>
40
39
  </div>
41
- <div class="mb-3">
42
- <label for="username" class="form-label">Username</label>
43
- <input type="text" class="form-control" id="username">
44
- </div>
45
- <div class="mb-3">
46
- <label for="password" class="form-label">Password</label>
47
- <input type="password" class="form-control" id="password">
40
+ <div class="mb-3" id="login">
41
+ <div class="mb-3">
42
+ <label for="registeredApp" class="form-label">Registered app</label>
43
+ <select id="registeredApp" class="form-select">
44
+ <option selected value="Midea SmartHome (MSmartHome)">Midea SmartHome (MSmartHome)</option>
45
+ <option value="NetHome Plus">NetHome Plus</option>
46
+ <option value="Meiju">Meiju</option>
47
+ <option value="Artison Clima">Artison Clima</option>
48
+ </select>
49
+ </div>
50
+ <div class="mb-3 hide bg-warning-subtle border-warning-subtle p-3 rounded-2" id="meijuWarning">
51
+ The Meiju app disabled the process of obtaining the required token/key pair for protocol version 3 devices.
52
+ When choosing Meiju, it will utilize the default Midea SmartHome profile to retrieve the token/key pair.
53
+ </div>
54
+ <div id="userPass">
55
+ <div class="mb-3">
56
+ <label for="username" class="form-label">Username</label>
57
+ <input type="text" class="form-control" id="username">
58
+ </div>
59
+ <div class="mb-3">
60
+ <label for="password" class="form-label">Password</label>
61
+ <input type="password" class="form-control" id="password">
62
+ </div>
63
+ </div>
48
64
  </div>
49
65
  <div class="mb-3">
50
66
  <label for="ip" class="form-label">IP Addresses (comma or space separated list for when devices are on
@@ -82,6 +98,19 @@
82
98
 
83
99
  </div>
84
100
 
101
+ <style>
102
+
103
+ .hide {
104
+ display: none;
105
+ }
106
+
107
+ .disabled {
108
+ pointer-events: none;
109
+ opacity: 0.5;
110
+ }
111
+ </style>
112
+
113
+
85
114
  <script>
86
115
 
87
116
  (async () => {
@@ -174,6 +203,30 @@
174
203
  createForm(configSchema, configuration);
175
204
  homebridge.hideSpinner();
176
205
 
206
+ const loginSection = document.getElementById('login')
207
+ const userPass = document.getElementById('userPass');
208
+ const meijuWarning = document.getElementById('meijuWarning');
209
+
210
+ document.getElementById('useDefaultProfile').addEventListener('change', function (e) {
211
+ if (e.target.checked === true) {
212
+ loginSection.classList.add('disabled');
213
+ } else {
214
+ loginSection.classList.remove('disabled');
215
+ }
216
+ });
217
+
218
+ document.getElementById('registeredApp').addEventListener('change', function (e) {
219
+ if (e.target.value === 'Meiju') {
220
+ meijuWarning.classList.remove('hide');
221
+ userPass.classList.add('disabled');
222
+ } else {
223
+ meijuWarning.classList.add('hide');
224
+ userPass.classList.remove('disabled');
225
+ }
226
+ });
227
+
228
+
229
+
177
230
  /*********************************************************************
178
231
  * Discover button clicked....
179
232
  */
@@ -183,6 +236,9 @@
183
236
  const username = document.getElementById('username').value;
184
237
  const password = document.getElementById('password').value;
185
238
  const registeredApp = document.getElementById('registeredApp').value;
239
+ const isMeiju = registeredApp === 'Meiju';
240
+ const useDefaultProfile = document.getElementById('useDefaultProfile').checked === true || isMeiju;
241
+
186
242
  let ipAddrs = document.getElementById('ip').value ? document.getElementById('ip').value.split(/[\s,]+/) : [];
187
243
 
188
244
  homebridge.showSpinner();
@@ -190,7 +246,7 @@
190
246
 
191
247
  console.info(`Request login...`);
192
248
  try {
193
- await homebridge.request('/login', { username, password, registeredApp });
249
+ await homebridge.request('/login', { username, password, registeredApp, useDefaultProfile });
194
250
  } catch (e) {
195
251
  homebridge.toast.error(e.message);
196
252
  homebridge.hideSpinner();
@@ -201,6 +257,7 @@
201
257
  const table = document.getElementById('discoverTable').getElementsByTagName('tbody')[0];
202
258
  table.innerHTML = '';
203
259
 
260
+
204
261
  // Merge current config over to the server so discover has latest device config...
205
262
  let currentConfig = await homebridge.getPluginConfig();
206
263
  currentConfig = currentConfig.length > 0 ? currentConfig[0] : {};
@@ -218,7 +275,7 @@
218
275
  return (regexIPv4.test(ip));
219
276
  });
220
277
  if (!validIPs) {
221
- homebride.toast.error(`Invalid IP address provided`);
278
+ homebridge.toast.error(`Invalid IP address provided`);
222
279
  homebridge.hideSpinner();
223
280
  // Abort if we have invalid IP address
224
281
  return;
@@ -246,12 +303,8 @@
246
303
  tr.insertCell().appendChild(document.createTextNode(device.version));
247
304
  if (device.version === 3) {
248
305
  tr.insertCell().appendChild(document.createTextNode(
249
- device.token ?
250
- device.token.slice(0, 6) + '...' + device.token.slice(-4) :
251
- 'token missing' + '\n' +
252
- device.key ?
253
- device.key.slice(0, 6) + '...' + device.key.slice(-4) :
254
- 'key missing'
306
+ (device.token ? (device.token.slice(0, 6) + '...' + device.token.slice(-4)) : 'token missing') + '\n' +
307
+ (device.key ? (device.key.slice(0, 6) + '...' + device.key.slice(-4)) : 'key missing')
255
308
  ));
256
309
  } else {
257
310
  tr.insertCell().appendChild(document.createTextNode('not needed'));
@@ -10,11 +10,17 @@
10
10
  const { HomebridgePluginUiServer, RequestError } = require('@homebridge/plugin-ui-utils');
11
11
  const Discover = require('../dist/core/MideaDiscover.js').default;
12
12
  const CloudFactory = require('../dist/core/MideaCloud.js').default;
13
- const { DeviceType, TCPMessageType, ProtocolVersion } = require("../dist/core/MideaConstants.js");
13
+ const { DeviceType, TCPMessageType, ProtocolVersion, Endianness } = require("../dist/core/MideaConstants.js");
14
14
  const { LocalSecurity } = require("../dist/core/MideaSecurity.js");
15
15
  const { PromiseSocket } = require("../dist/core/MideaUtils.js");
16
16
  const { defaultConfig, defaultDeviceConfig } = require('../dist/platformUtils.js');
17
17
 
18
+ const DEFAULT_ACCOUNT = [
19
+ BigInt("39182118275972017797890111985649342047468653967530949796945843010512"),
20
+ BigInt("29406100301096535908214728322278519471982973450672552249652548883645"),
21
+ BigInt("39182118275972017797890111985649342050088014265865102175083010656997")
22
+ ]
23
+
18
24
  var _ = require('lodash');
19
25
 
20
26
  /*********************************************************************
@@ -81,8 +87,14 @@ class UiServer extends HomebridgePluginUiServer {
81
87
  this.security = new LocalSecurity();
82
88
  this.promiseSocket = new PromiseSocket(this.logger, config?.logRecoverableErrors ? config.logRecoverableErrors : false);
83
89
 
84
- this.onRequest('/login', async ({ username, password, registeredApp }) => {
90
+ this.onRequest('/login', async ({ username, password, registeredApp, useDefaultProfile }) => {
85
91
  try {
92
+ if (useDefaultProfile) {
93
+ this.logger.debug(`Using default profile.`);
94
+ registeredApp = 'Midea SmartHome (MSmartHome)';
95
+ username = Buffer.from((DEFAULT_ACCOUNT[0] ^ DEFAULT_ACCOUNT[1]).toString(16), 'hex').toString('ascii')
96
+ password = Buffer.from((DEFAULT_ACCOUNT[0] ^ DEFAULT_ACCOUNT[2]).toString(16), 'hex').toString('ascii')
97
+ }
86
98
  this.cloud = CloudFactory.createCloud(username, password, registeredApp);
87
99
  if (username && password && registeredApp) {
88
100
  await this.cloud.login();
@@ -90,6 +102,7 @@ class UiServer extends HomebridgePluginUiServer {
90
102
  } catch (e) {
91
103
  const msg = e instanceof Error ? e.stack : e;
92
104
  this.logger.warn(`Login failed:\n${msg}`);
105
+ throw new RequestError(`Login failed! Check the logs for more information.`);
93
106
  }
94
107
  });
95
108
 
@@ -149,9 +162,9 @@ class UiServer extends HomebridgePluginUiServer {
149
162
  // works or having tried both.
150
163
  while (i <= 1 && !connected) {
151
164
  // Start with big-endianess as it is more likely to succeed.
152
- const endianess = i === 0 ? 'big' : 'little';
165
+ const endianess = i === 0 ? Endianness.Little : Endianness.Big;
153
166
  try {
154
- const [token, key] = await this.cloud.getToken(device.id, endianess);
167
+ const [token, key] = await this.cloud.getTokenKey(device.id, endianess);
155
168
  device.token = token ? token.toString('hex') : undefined;
156
169
  device.key = key ? key.toString('hex') : undefined;
157
170
  await this.authenticate(device);
@@ -226,6 +239,12 @@ class UiServer extends HomebridgePluginUiServer {
226
239
  case DeviceType.DEHUMIDIFIER:
227
240
  device['displayName'] = 'Dehumidifier';
228
241
  break;
242
+ case DeviceType.ELECTRIC_WATER_HEATER:
243
+ device['displayName'] = 'Electric Water Heater';
244
+ break;
245
+ case DeviceType.GAS_WATER_HEATER:
246
+ device['displayName'] = 'Gas Water Heater';
247
+ break;
229
248
  default:
230
249
  device['displayName'] = 'Unknown';
231
250
  break;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "displayName": "Homebridge Midea Platform",
3
3
  "name": "homebridge-midea-platform",
4
- "version": "0.4.13",
4
+ "version": "1.0.0",
5
5
  "description": "Homebridge plugin for Midea devices",
6
6
  "license": "Apache-2.0",
7
7
  "repository": {
@@ -24,6 +24,12 @@
24
24
  "format": "prettier --log-level warn --write \"**/*.ts\" && npm run lint -- --fix",
25
25
  "prepare": "husky install"
26
26
  },
27
+ "lint-staged": {
28
+ "*.ts": [
29
+ "prettier --write",
30
+ "eslint --fix"
31
+ ]
32
+ },
27
33
  "keywords": [
28
34
  "homebridge-plugin"
29
35
  ],
@@ -46,6 +52,7 @@
46
52
  "eslint-plugin-prettier": "^5.0.1",
47
53
  "homebridge": "^1.6.0",
48
54
  "husky": "^8.0.0",
55
+ "lint-staged": "^15.2.2",
49
56
  "nodemon": "^3.0.1",
50
57
  "prettier": "^3.0.3",
51
58
  "rimraf": "^3.0.2",