@switchbot/homebridge-switchbot 5.2.0 → 5.2.1-beta.1

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.
@@ -150,192 +150,95 @@ node -p "const v=require('./package.json').version.split('.'); v[2]=parseInt(v[2
150
150
  3. Proceed with patch increment as fallback
151
151
  4. Create or target the appropriate beta branch based on version increment
152
152
 
153
+ ## Following node-switchbot Changes
154
+
155
+ node-switchbot tracks [pySwitchbot](https://github.com/sblibs/pySwitchbot). After node-switchbot
156
+ ships a change, this repo usually needs a follow-up PR so the plugin can use it:
157
+
158
+ 1. Read the node-switchbot PR/release and list what consumers see: new device types, renamed or
159
+ split model identities (`modelName`, OpenAPI `deviceType`), new status fields, new commands.
160
+ 2. Update `src/device-types.ts` (`DEVICE_TYPES`, `DEVICE_TYPE_NORMALIZATION_MAP`, including
161
+ lowercase product codes such as `woplugjp`), `DEVICE_CLASS_MAP` in `src/deviceFactory.ts`, and
162
+ accessory behaviour in `src/devices/genericDevice.ts`.
163
+ 3. Make sure no existing saved `configDeviceType` stops resolving (normalization map or
164
+ `src/homebridge-ui/utils/device-migration.ts`).
165
+ 4. Bump `node-switchbot` in `package.json` only to a version already published on npm.
166
+ 5. Add `createDevice` tests under `test/device/` asserting each affected type's accessory class.
167
+ 6. Run build, test and lint; commit the regenerated `config.schema.json` if `DEVICE_TYPES` changed.
168
+
153
169
  ## Working Effectively
154
170
 
155
- ### Bootstrap and Build Process
156
- Run these commands in sequence to set up and build the project:
157
-
158
- 1. **Install dependencies:**
159
- ```bash
160
- npm install --legacy-peer-deps
161
- ```
162
- - CRITICAL: Must use `--legacy-peer-deps` flag due to TypeDoc dependency conflicts
163
- - Takes approximately 30-45 seconds
164
- - This is a known workaround documented in the codebase
165
-
166
- 2. **Build the project:**
167
- ```bash
168
- npm run build
169
- ```
170
- - Takes approximately 7-10 seconds
171
- - Compiles TypeScript and copies UI files
172
- - Output goes to `dist/` directory
173
-
174
- 3. **Run tests:**
175
- ```bash
176
- npm run test
177
- ```
178
- - Takes approximately 2-3 seconds
179
- - Currently minimal test suite (1 test file)
180
- - Use `npm run test:watch` for continuous testing during development
181
-
182
- 4. **Generate documentation:**
183
- ```bash
184
- npm run docs
185
- ```
186
- - Takes approximately 5-10 seconds
187
- - Generates TypeDoc documentation in `docs/` directory
188
-
189
- ### Full Build and Validation Workflow
190
- Complete validation workflow (approximately 30-45 seconds total):
191
- ```bash
192
- npm install --legacy-peer-deps && npm run build && npm run test && npm run docs && npm run docs:lint && npm run docs:theme
193
- ```
194
- - NEVER CANCEL: Set timeout to 90+ seconds for this complete workflow
195
- - This validates TypeScript compilation, plugin loading, and documentation generation
196
-
197
- ### Development Commands
198
-
199
- - **Watch mode for development:**
200
- ```bash
201
- npm run watch
202
- ```
203
- - Builds, links plugin locally, and starts nodemon for file watching
204
- - Requires local Homebridge installation
205
-
206
- - **Clean build artifacts:**
207
- ```bash
208
- npm run clean
209
- ```
210
- - Removes `dist/` directory
211
-
212
- - **Check for outdated dependencies:**
213
- ```bash
214
- npm run check
215
- ```
216
- - WARNING: This will fail due to dependency conflicts - use with `--legacy-peer-deps`
217
-
218
- ### Known Issues and Workarounds
219
-
220
- - **Linting fails:** `npm run lint` and `npm run lint:fix` currently fail due to ESLint configuration conflicts with import/extensions rule. Document any code style issues manually.
221
-
222
- - **Dependency conflicts:** Always use `--legacy-peer-deps` flag for npm install due to TypeDoc theme dependency version mismatches.
223
-
224
- - **Coverage and docs artifacts:** Add `coverage/` and `docs/` to `.gitignore` to prevent committing build artifacts.
225
-
226
- ## Validation Scenarios
227
-
228
- ### Manual Plugin Validation
229
- After making changes, ALWAYS run these validation steps:
230
-
231
- 1. **Verify plugin builds and loads:**
232
- ```bash
233
- npm run build && node -e "
234
- try {
235
- const plugin = require('./dist/index.js');
236
- console.log('Plugin loads successfully');
237
- console.log('Plugin type:', typeof plugin.default);
238
- } catch (e) {
239
- console.error('Plugin load failed:', e.message);
240
- }"
241
- ```
242
-
243
- 2. **Test plugin registration:**
244
- - Plugin exports a default function that registers with Homebridge
245
- - Uses `PLUGIN_NAME = '@switchbot/homebridge-switchbot'` and `PLATFORM_NAME = 'SwitchBot'`
246
- - Main platform class is `SwitchBotPlatform`
247
-
248
- 3. **Validate TypeScript compilation:**
249
- - Must have no TypeScript errors
250
- - Check `dist/` directory contains all expected `.js`, `.d.ts`, and `.map` files
251
-
252
- 4. **Test documentation generation:**
253
- ```bash
254
- npm run docs:lint
255
- ```
256
- - Must pass without warnings when using `--treatWarningsAsErrors`
257
-
258
- ### Code Coverage Validation
259
- Run comprehensive test coverage analysis:
171
+ Node `^22 || ^24`. Run these in order; each must pass before you commit:
172
+
260
173
  ```bash
261
- npm run test-coverage
174
+ npm ci # CI uses: npm ci --legacy-peer-deps
175
+ npm run build # syncs DEVICE_TYPES into config.schema.json, cleans dist/, tsc, copies + builds the plugin UI
176
+ npm run test # vitest run
177
+ npm run lint # eslint (src, test, UI html/js) then tsc --noEmit
262
178
  ```
263
- - Current coverage is minimal (~1.36% overall)
264
- - Coverage reports generated in `coverage/` directory
179
+
180
+ - `npm run lint:fix` fixes most style issues.
181
+ - `npm run build` needs `rsync` (used by `plugin-ui`).
182
+ - `npm run docs` / `npm run docs:lint` generate and check TypeDoc output; `prepublishOnly` runs them.
183
+ - `npm run watch` builds, links the plugin and runs nodemon against a local Homebridge;
184
+ `npm run watch:ui` also starts a dev Homebridge with Config UI X.
185
+
186
+ ### Known pitfalls
187
+
188
+ - **Never commit compiled output under `src/`.** `.gitignore` excludes `src/**/*.js` and
189
+ `src/**/*.js.map`. Vitest resolves `./foo.js` imports to a real `src/foo.js` before `src/foo.ts`,
190
+ so a stale compiled copy silently replaces the source in tests.
191
+ - **`config.schema.json` is generated** from `DEVICE_TYPES` in `src/device-types.ts` during
192
+ `npm run build`. When `DEVICE_TYPES` changes, commit the regenerated schema in the same change.
193
+ - **`DEVICE_CLASS_MAP` keys in `src/deviceFactory.ts` are lowercase.** Unmapped types fall back to
194
+ `GenericDevice`, so add a test for every new type.
265
195
 
266
196
  ## Codebase Navigation
267
197
 
268
198
  ### Project Structure
269
199
  ```
270
- ├── src/ # Source TypeScript files
271
- │ ├── device/ # Device implementations (21 files)
272
- │ │ ├── device.ts # Base device class (deviceBase)
273
- │ │ ├── bot.ts # SwitchBot Bot device
274
- │ │ ├── curtain.ts # SwitchBot Curtain device
275
- │ │ └── ... # Other device implementations
276
- │ ├── irdevice/ # IR device implementations (10 files)
277
- │ │ ├── irdevice.ts # Base IR device class
278
- │ │ ├── tv.ts # TV IR control
279
- │ │ └── ... # Other IR device implementations
280
- │ ├── homebridge-ui/ # Plugin configuration UI
281
- │ │ ├── server.ts # UI backend server
282
- │ │ └── public/ # UI frontend files
283
- │ ├── platform.ts # Main platform class (SwitchBotPlatform)
284
- │ ├── settings.ts # Configuration interfaces and constants
285
- │ ├── utils.ts # Utility functions
286
- │ └── index.ts # Plugin entry point
287
- ├── config.schema.json # Homebridge configuration schema
288
- ├── dist/ # Compiled JavaScript output
289
- ├── docs/ # Generated TypeDoc documentation
290
- └── coverage/ # Test coverage reports
200
+ ├── src/
201
+ │ ├── index.ts # Plugin entry point (registers one platform that proxies HAP or Matter)
202
+ │ ├── SwitchBotHAPPlatform.ts # HomeKit (HAP) platform
203
+ │ ├── SwitchBotMatterPlatform.ts # Matter platform
204
+ │ ├── switchbotClient.ts # Wrapper around node-switchbot (BLE + OpenAPI)
205
+ │ ├── deviceFactory.ts # Device type -> accessory class (DEVICE_CLASS_MAP, createDevice)
206
+ │ ├── device-types.ts # DEVICE_TYPES (valid types) + DEVICE_TYPE_NORMALIZATION_MAP (aliases)
207
+ │ ├── deviceCommandMapper.ts # Per-type command handlers
208
+ │ ├── devices/
209
+ │ │ ├── deviceBase.ts # Base accessory class
210
+ │ │ └── genericDevice.ts # GenericDevice and every concrete accessory class
211
+ │ ├── homebridge-ui/ # Config UI X custom UI (server, endpoints, utils, public/)
212
+ │ ├── settings.ts # Config interfaces and constants
213
+ │ ├── errors.ts
214
+ │ └── utils.ts
215
+ ├── test/ # Vitest specs (client, device, platform, homebridge-ui, utils, e2e)
216
+ ├── scripts/ # sync-device-types, build-ui, free-dev-ports
217
+ ├── config.schema.json # Homebridge config schema (configDeviceType enum is generated)
218
+ └── dist/ # Build output (not committed)
291
219
  ```
292
220
 
293
- ### Key Development Areas
294
-
295
- - **Adding new devices:** Extend `src/device/device.ts` base class, implement in new file under `src/device/`
296
- - **Adding IR devices:** Extend `src/irdevice/irdevice.ts` base class, implement in new file under `src/irdevice/`
297
- - **Configuration changes:** Update `src/settings.ts` and `config.schema.json`
298
- - **Platform modifications:** Edit `src/platform.ts` (main class with 2800+ lines)
299
- - **Utility functions:** Modify `src/utils.ts` for shared functionality
300
-
301
- ### Device Connection Types
302
- This plugin supports two connection methods to SwitchBot devices:
303
- 1. **OpenAPI Connection:** Requires SwitchBot Cloud API token/secret, works through SwitchBot Hub
304
- 2. **BLE (Bluetooth Low Energy):** Direct device connection, requires device MAC addresses
305
-
306
221
  ### Common Development Tasks
307
222
 
308
- - **Testing device implementations:** Focus on `src/device/` and `src/irdevice/` directories
309
- - **Configuration UI changes:** Modify `src/homebridge-ui/` files and rebuild with `npm run plugin-ui`
310
- - **Platform logic changes:** Edit `src/platform.ts` - the main orchestration file
311
- - **Adding device support:** Create new device class extending `deviceBase` or IR device class
312
-
313
- ### Important File Relationships
314
- - Always update both device implementation AND platform.ts when adding new device types
315
- - Configuration schema changes require updates to settings.ts interfaces
316
- - Device MAC address validation is handled in utils.ts (`formatDeviceIdAsMac`)
317
- - BLE connection handling is in the base device classes
318
-
319
- ## Build Time Expectations
223
+ - **New device type or identity from node-switchbot:** add it to `DEVICE_TYPES` and/or
224
+ `DEVICE_TYPE_NORMALIZATION_MAP` in `src/device-types.ts`, map it in `DEVICE_CLASS_MAP` in
225
+ `src/deviceFactory.ts`, then add a `createDevice` test under `test/device/`.
226
+ - **New accessory behaviour:** add or extend a class in `src/devices/genericDevice.ts`.
227
+ - **Saved configs that would stop resolving:** handle them in the normalization map or
228
+ `src/homebridge-ui/utils/device-migration.ts`.
229
+ - **Config UI changes:** edit `src/homebridge-ui/`, then `npm run build`.
230
+ - **node-switchbot upgrades:** bump the dependency only to a version already published on npm.
320
231
 
321
- - **npm install --legacy-peer-deps:** 30-45 seconds
322
- - **npm run build:** 7-10 seconds
323
- - **npm run test:** 2-3 seconds
324
- - **npm run docs:** 5-10 seconds
325
- - **Complete validation workflow:** 30-45 seconds total
326
- - **NEVER CANCEL:** Always wait for builds to complete, set timeouts to 90+ seconds for safety
232
+ ### Device Connection Types
233
+ 1. **OpenAPI:** SwitchBot Cloud token/secret; works through a SwitchBot Hub.
234
+ 2. **BLE:** direct Bluetooth connection via node-switchbot (Linux/macOS).
327
235
 
328
236
  ## CI/CD Integration
329
237
 
330
- The project uses GitHub Actions for CI:
331
- - **Build workflow:** `.github/workflows/build.yml` runs Node.js build and test, then ESLint
332
- - **Required checks:** Build must pass, tests must pass
333
- - **Known CI issue:** ESLint currently fails due to configuration conflicts
334
-
335
- Always run the complete validation workflow locally before committing:
336
- ```bash
337
- npm run build && npm run test && npm run docs:lint
338
- ```
238
+ - `.github/workflows/ci.yml`: build, test and lint on Node 22 and 24 for pushes to `latest`/`beta-*`
239
+ and PRs to `latest`/`beta-*`.
240
+ - `.github/workflows/release.yml`: pushes to `beta-*` publish a beta to npm; pushes to `latest`
241
+ publish a stable release.
339
242
 
340
243
  ## Complete Workflow Examples
341
244
 
@@ -0,0 +1,100 @@
1
+ #!/bin/env node
2
+
3
+ /**
4
+ * This scripts queries the npm registry to pull out the latest version for a given tag.
5
+ */
6
+
7
+ import assert from 'node:assert'
8
+ import child_process from 'node:child_process'
9
+ import fs from 'node:fs'
10
+ import process from 'node:process'
11
+
12
+ const BRANCH_VERSION_PATTERN = /^([A-Z]+)-(\d+\.\d+\.\d+)$/i
13
+
14
+ // Load the contents of the package.json file
15
+ const packageJSON = JSON.parse(fs.readFileSync('package.json', 'utf8'))
16
+
17
+ const refArgument = process.argv[2]
18
+ const tagArgument = process.argv[3] || 'latest'
19
+
20
+ if (!refArgument) {
21
+ console.error('ref argument is missing')
22
+ console.error('Usage: npm-version-script-esm.js <ref> [tag]')
23
+ process.exit(1)
24
+ }
25
+
26
+ /**
27
+ * Queries the NPM registry for the latest version for the provided base version and tag.
28
+ * If the tag is latest, then the base version is returned if it exists. For other tags, the latest
29
+ * version found for that base version and tag is returned.
30
+ * @param baseVersion The base version to query for, e.g. 2.0.0
31
+ * @param tag The tag to query for, e.g. beta or latest
32
+ * @returns {string} Returns the version, or '' if not found
33
+ */
34
+ function getTagVersionFromNpm(baseVersion, tag) {
35
+ try {
36
+ return JSON.parse(child_process.execSync(`npm info ${packageJSON.name} versions --json`).toString('utf8').trim())
37
+ .filter(v => tag === 'latest' ? v === baseVersion : v.startsWith(`${baseVersion}-${tag}.`)) // find all published versions for this base version and tag
38
+ .reduce((_, current) => current, '') // choose the last as they're sorted in ascending order, or '' if there are none
39
+ } catch (e) {
40
+ console.error(`Failed to query the npm registry for the latest version for tag: ${tag}`, e)
41
+ // throw e;
42
+ return ''
43
+ }
44
+ }
45
+
46
+ function desiredTargetVersion(ref) {
47
+ // ref is a GitHub action ref string
48
+ if (ref.startsWith('refs/pull/')) {
49
+ throw new Error('The version script was executed inside a PR!')
50
+ }
51
+
52
+ assert(ref.startsWith('refs/heads/'))
53
+ const branchName = ref.slice('refs/heads/'.length)
54
+
55
+ const results = branchName.match(BRANCH_VERSION_PATTERN)
56
+ if (results !== null) {
57
+ if (results[1] !== tagArgument) {
58
+ console.warn(`The base branch name (${results[1]}) differs from the tag name ${tagArgument}`)
59
+ }
60
+
61
+ return results[2]
62
+ }
63
+
64
+ throw new Error(`Malformed branch name for ref: ${ref}. Can't derive the base version. Use a branch name like: beta-x.x.x or alpha-x.x.x`)
65
+ }
66
+
67
+ // derive the base version from the branch ref
68
+ const baseVersion = desiredTargetVersion(refArgument)
69
+
70
+ // query the npm registry for the latest version of the provided tag name
71
+ const latestReleasedVersion = getTagVersionFromNpm(baseVersion, tagArgument) // e.g. 0.7.0-beta.12
72
+
73
+ let publishTag
74
+
75
+ if (latestReleasedVersion) {
76
+ console.warn(`Latest published version for ${baseVersion} with tag ${tagArgument} is ${latestReleasedVersion}`)
77
+ publishTag = latestReleasedVersion // set this released beta or alpha to be incremented
78
+ } else {
79
+ console.warn(`No published versions for ${baseVersion} with tag ${tagArgument}`)
80
+ publishTag = baseVersion // start off with a new beta or alpha version
81
+ }
82
+
83
+ if (packageJSON.version !== publishTag) {
84
+ // report the change for CI
85
+ console.warn(`Changing version in package.json from ${packageJSON.version} to ${publishTag}`)
86
+
87
+ // save the package.json
88
+ // JSON.stringify does not end with a newline, but npm writes both of these
89
+ // files with one. Leaving it off makes the file fail an eol-last lint rule,
90
+ // which breaks any repo whose prepublishOnly lints the whole tree.
91
+ packageJSON.version = publishTag
92
+ fs.writeFileSync('package.json', `${JSON.stringify(packageJSON, null, 2)}\n`)
93
+
94
+ // perform the same change to the package-lock.json
95
+ const packageLockJSON = JSON.parse(fs.readFileSync('package-lock.json', 'utf8'))
96
+ packageLockJSON.version = publishTag
97
+ fs.writeFileSync('package-lock.json', `${JSON.stringify(packageLockJSON, null, 2)}\n`)
98
+ } else {
99
+ console.warn(`Leaving version in package.json at ${packageJSON.version}`)
100
+ }
@@ -4,7 +4,7 @@ on:
4
4
  push:
5
5
  branches: ["main", "latest", "beta-*"]
6
6
  pull_request:
7
- branches: ["main", "latest"]
7
+ branches: ["main", "latest", "beta-*"]
8
8
 
9
9
  permissions:
10
10
  contents: read
@@ -33,3 +33,6 @@ jobs:
33
33
 
34
34
  - name: Run tests
35
35
  run: npm run test
36
+
37
+ - name: Lint
38
+ run: npm run lint
package/CHANGELOG.md CHANGED
@@ -1,3 +1,12 @@
1
+ # [5.2.0](https://github.com/OpenWonderLabs/homebridge-switchbot/compare/v5.1.1...v5.2.0) (2026-10-09)
2
+
3
+
4
+ ### Features
5
+
6
+ * add a CarbonDioxideSensor service for the Meter Pro (CO2) ([#1413](https://github.com/OpenWonderLabs/homebridge-switchbot/issues/1413)) ([588a59d](https://github.com/OpenWonderLabs/homebridge-switchbot/commit/588a59d8adcde2d77a6c3fb1bf703c1d5cf2cf01)), closes [#1197](https://github.com/OpenWonderLabs/homebridge-switchbot/issues/1197) [#1112](https://github.com/OpenWonderLabs/homebridge-switchbot/issues/1112) [#1197](https://github.com/OpenWonderLabs/homebridge-switchbot/issues/1197)
7
+
8
+
9
+
1
10
  # [5.2.0](https://github.com/OpenWonderLabs/homebridge-switchbot/compare/v5.1.0...v5.2.0) (2026-10-08)
2
11
 
3
12
  ### Bug Fixes
package/CLAUDE.md ADDED
@@ -0,0 +1,13 @@
1
+ # homebridge-switchbot
2
+
3
+ The project guide is shared with Copilot and imported here. It covers reference priority, branch
4
+ targeting, following node-switchbot changes, commands, structure and pitfalls:
5
+
6
+ @.github/copilot-instructions.md
7
+
8
+ ## Claude-specific notes
9
+
10
+ - Conventional Commits (`feat:`, `fix:`, `docs:`, `test:`, `chore:`).
11
+ - If the right target branch is unclear, ask before opening the PR.
12
+ - PR description: what changed, why (link the node-switchbot PR/release), and any config migration
13
+ users need.
package/README.md CHANGED
@@ -176,6 +176,8 @@ Click **Save Advanced Settings** to apply changes. These settings match the opti
176
176
  - [SwitchBot Hub Mini](https://www.switch-bot.com/products/switchbot-hub-mini), [SwitchBot Hub 2](https://us.switch-bot.com/products/switchbot-hub-2), or [SwitchBot Hub 3](https://us.switch-bot.com/products/switchbot-hub-3) Required
177
177
  - Enable Cloud Services for Device on SwitchBot App
178
178
  - [SwitchBot Bot](https://www.switch-bot.com/products/switchbot-bot)
179
+ - **Bot HomeKit Display** (`botDisplay` in `config.json`) can show a Bot as a Switch, Door, Garage Door, Window, Window Covering, Lock, Outlet, Fan, Faucet, or Stateful Programmable Switch. The default is Switch. The legacy per-device `type` setting still works. Stateful Programmable Switch needs a third-party Home app.
180
+ - Set **Bot Physical Mode** (`mode`) to match the SwitchBot app: **Press** sends one `press` command when activated and returns HomeKit to its resting state; **Switch** sends `turnOn`/`turnOff`. If unset, Door uses Press and other displays use Switch. A Bot has no sensor for the door, window, lock, or other object it operates; their HomeKit state reflects only the Bot control, not the object's physical state.
179
181
  - Supports OpenAPI & Bluetooth Low Energy (BLE) Connections
180
182
  - If using OpenAPI:
181
183
  - [SwitchBot Hub Mini](https://www.switch-bot.com/products/switchbot-hub-mini), [SwitchBot Hub 2](https://us.switch-bot.com/products/switchbot-hub-2), or [SwitchBot Hub 3](https://us.switch-bot.com/products/switchbot-hub-3) Required
@@ -169,6 +169,36 @@
169
169
  "remote with screen"
170
170
  ]
171
171
  },
172
+ "botDisplay": {
173
+ "type": "string",
174
+ "title": "Bot HomeKit Display",
175
+ "description": "For a Bot only: choose its HomeKit service. This changes the Home app presentation, not the Bot's physical mode. The Bot cannot measure the position or state of the device it presses.",
176
+ "enum": [
177
+ "switch",
178
+ "door",
179
+ "garagedoor",
180
+ "window",
181
+ "windowcovering",
182
+ "lock",
183
+ "outlet",
184
+ "fan",
185
+ "faucet",
186
+ "stateful"
187
+ ],
188
+ "default": "switch"
189
+ },
190
+ "mode": {
191
+ "type": "string",
192
+ "title": "Bot Physical Mode",
193
+ "description": "For a Bot only: match its mode in the SwitchBot app. Press sends one press and returns the HomeKit control to its resting state. Switch sends turnOn/turnOff. Legacy Multi-Press currently sends one press. If unset, Door uses Press and other displays use Switch.",
194
+ "enum": [
195
+ "",
196
+ "press",
197
+ "switch",
198
+ "multipress"
199
+ ],
200
+ "default": ""
201
+ },
172
202
  "connectionPreference": {
173
203
  "type": "string",
174
204
  "title": "Connection Preference",
@@ -1 +1 @@
1
- {"version":3,"file":"SwitchBotHAPPlatform.d.ts","sourceRoot":"","sources":["../src/SwitchBotHAPPlatform.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAA;AAC1D,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAO7D;;;;;;;;;;;;;;;;;;;GAmBG;AACH,qBAAa,oBAAoB;IAC/B,8BAA8B;IAC9B,GAAG,EAAE,GAAG,GAAG,SAAS,CAAA;IACpB,iCAAiC;IACjC,GAAG,EAAE,MAAM,CAAA;IACX,2BAA2B;IAC3B,MAAM,EAAE,qBAAqB,CAAA;IAC7B,mCAAmC;IACnC,OAAO,EAAE,GAAG,EAAE,CAAK;IACnB,gDAAgD;IAChD,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAC7B,sDAAsD;IACtD,OAAO,CAAC,cAAc,CAAa;IACnC,0CAA0C;IAC1C,OAAO,CAAC,oBAAoB,CAA8B;IAC1D,4EAA4E;IAC5E,OAAO,CAAC,cAAc,CAA0H;IAEhJ,4CAA4C;IAC5C,OAAO,CAAC,iBAAiB,CAAyC;IAClE,wCAAwC;IACxC,OAAO,CAAC,iBAAiB,CAA8B;IACvD,2CAA2C;IAC3C,OAAO,CAAC,oBAAoB,CAAI;IAChC,yDAAyD;IACzD,OAAO,CAAC,gBAAgB,CAAI;IAE5B;;;;;OAKG;gBACS,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,cAAc,EAAE,GAAG,CAAC,EAAE,GAAG;IA2C1D;;;;;;OAMG;IACG,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC;IAgElC;;;;;;;;;;;;;;;;;;;;OAoBG;YACW,sBAAsB;IAiKpC;;;OAGG;IACH,mBAAmB,IAAI,MAAM;IAI7B;;;OAGG;IACH,mBAAmB,IAAI,IAAI;IAS3B;;;OAGG;IACH,OAAO,CAAC,aAAa;IAUrB;;;;;;OAMG;YACW,qBAAqB;IAiCnC;;;;OAIG;IACH,QAAQ,IAAI,IAAI;IAQhB;;;;;OAKG;IACH;;;;;OAKG;YACW,gBAAgB;IA8B9B;;;OAGG;IACH,OAAO,CAAC,cAAc;IAMtB,OAAO,CAAC,oBAAoB;IAoI5B;;;;OAIG;IACG,kBAAkB,CAAC,SAAS,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;IAUvD;;;;OAIG;IACH,wBAAwB,CAAC,CAAC,SAAS,EAAE,GAAG,GAAG,IAAI;CAShD;AAED,eAAe,oBAAoB,CAAA"}
1
+ {"version":3,"file":"SwitchBotHAPPlatform.d.ts","sourceRoot":"","sources":["../src/SwitchBotHAPPlatform.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAA;AAC1D,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAO7D;;;;;;;;;;;;;;;;;;;GAmBG;AACH,qBAAa,oBAAoB;IAC/B,8BAA8B;IAC9B,GAAG,EAAE,GAAG,GAAG,SAAS,CAAA;IACpB,iCAAiC;IACjC,GAAG,EAAE,MAAM,CAAA;IACX,2BAA2B;IAC3B,MAAM,EAAE,qBAAqB,CAAA;IAC7B,mCAAmC;IACnC,OAAO,EAAE,GAAG,EAAE,CAAK;IACnB,gDAAgD;IAChD,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAC7B,sDAAsD;IACtD,OAAO,CAAC,cAAc,CAAa;IACnC,0CAA0C;IAC1C,OAAO,CAAC,oBAAoB,CAA8B;IAC1D,4EAA4E;IAC5E,OAAO,CAAC,cAAc,CAA0H;IAEhJ,4CAA4C;IAC5C,OAAO,CAAC,iBAAiB,CAAyC;IAClE,wCAAwC;IACxC,OAAO,CAAC,iBAAiB,CAA8B;IACvD,2CAA2C;IAC3C,OAAO,CAAC,oBAAoB,CAAI;IAChC,yDAAyD;IACzD,OAAO,CAAC,gBAAgB,CAAI;IAE5B;;;;;OAKG;gBACS,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,cAAc,EAAE,GAAG,CAAC,EAAE,GAAG;IA2C1D;;;;;;OAMG;IACG,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC;IAgElC;;;;;;;;;;;;;;;;;;;;OAoBG;YACW,sBAAsB;IA+KpC;;;OAGG;IACH,mBAAmB,IAAI,MAAM;IAI7B;;;OAGG;IACH,mBAAmB,IAAI,IAAI;IAS3B;;;OAGG;IACH,OAAO,CAAC,aAAa;IAYrB;;;;;;OAMG;YACW,qBAAqB;IAiCnC;;;;OAIG;IACH,QAAQ,IAAI,IAAI;IAQhB;;;;;OAKG;IACH;;;;;OAKG;YACW,gBAAgB;IA8B9B;;;OAGG;IACH,OAAO,CAAC,cAAc;IAMtB,OAAO,CAAC,oBAAoB;IAoI5B;;;;OAIG;IACG,kBAAkB,CAAC,SAAS,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;IAUvD;;;;OAIG;IACH,wBAAwB,CAAC,CAAC,SAAS,EAAE,GAAG,GAAG,IAAI;CAShD;AAED,eAAe,oBAAoB,CAAA"}
@@ -285,9 +285,11 @@ export class SwitchBotHAPPlatform {
285
285
  continue;
286
286
  }
287
287
  const service = accessory.getService(Service) || accessory.addService(Service);
288
- const charNames = Object.keys(s.characteristics || {});
288
+ const characteristicUUIDs = new Set(Object.keys(s.characteristics || {})
289
+ .map(name => hap.Characteristic[name]?.UUID)
290
+ .filter(Boolean));
289
291
  for (const existingChar of service.characteristics.slice()) {
290
- if (!charNames.includes(existingChar.displayName)) {
292
+ if (!characteristicUUIDs.has(existingChar.UUID)) {
291
293
  service.removeCharacteristic(existingChar);
292
294
  }
293
295
  }
@@ -316,21 +318,32 @@ export class SwitchBotHAPPlatform {
316
318
  service.getCharacteristic(Characteristic).onSet(async (value) => {
317
319
  await getterSetter.set(value);
318
320
  const refreshAfterSet = Array.isArray(getterSetter.refreshAfterSet) ? getterSetter.refreshAfterSet : [];
319
- for (const refreshCharName of refreshAfterSet) {
320
- const refreshGetterSetter = (s.characteristics || {})[refreshCharName];
321
- if (!refreshGetterSetter || typeof refreshGetterSetter.get !== 'function') {
322
- continue;
323
- }
324
- const RefreshCharacteristic = hap.Characteristic[refreshCharName];
325
- if (!RefreshCharacteristic) {
326
- continue;
327
- }
328
- try {
329
- service.getCharacteristic(RefreshCharacteristic).updateValue(await refreshGetterSetter.get());
330
- }
331
- catch (e) {
332
- this.log.debug?.(`Failed to refresh ${refreshCharName} after setting ${charName}`, e);
333
- }
321
+ // HAP stores the requested value after onSet resolves. Refresh on the next tick
322
+ // so a momentary control can return to its resting value.
323
+ if (refreshAfterSet.length > 0 || typeof getterSetter.afterSet === 'function') {
324
+ setTimeout(async () => {
325
+ for (const refreshCharName of refreshAfterSet) {
326
+ const refreshGetterSetter = (s.characteristics || {})[refreshCharName];
327
+ const RefreshCharacteristic = hap.Characteristic[refreshCharName];
328
+ if (!RefreshCharacteristic || typeof refreshGetterSetter?.get !== 'function') {
329
+ continue;
330
+ }
331
+ try {
332
+ service.getCharacteristic(RefreshCharacteristic).updateValue(await refreshGetterSetter.get());
333
+ }
334
+ catch (e) {
335
+ this.log.debug?.(`Failed to refresh ${refreshCharName} after setting ${charName}`, e);
336
+ }
337
+ }
338
+ if (typeof getterSetter.afterSet === 'function') {
339
+ try {
340
+ await getterSetter.afterSet(value, service, hap);
341
+ }
342
+ catch (e) {
343
+ this.log.debug?.(`Failed afterSet for ${charName}`, e);
344
+ }
345
+ }
346
+ }, 0);
334
347
  }
335
348
  });
336
349
  }
@@ -387,6 +400,8 @@ export class SwitchBotHAPPlatform {
387
400
  id: d.deviceId ?? d.id,
388
401
  type: d.configDeviceType ?? d.type,
389
402
  name: d.configDeviceName ?? d.name,
403
+ botDisplay: d.botDisplay ?? d.type,
404
+ mode: d.mode,
390
405
  })));
391
406
  }
392
407
  /**