@switchbot/homebridge-switchbot 5.1.0-beta.0 → 5.1.0-beta.10

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 (101) hide show
  1. package/.github/copilot-instructions.md +63 -0
  2. package/.github/scripts/normalize-changelog-header.cjs +86 -0
  3. package/.github/scripts/npm-version-script-esm-auto.js +201 -0
  4. package/.github/scripts/resolve-version-bump-esm.js +118 -0
  5. package/.github/workflows/release.yml +62 -13
  6. package/CHANGELOG.md +18 -0
  7. package/dist/{SwitchBotHAPPlatform.d.ts → Platform.HAP.d.ts} +2 -2
  8. package/dist/Platform.HAP.d.ts.map +1 -0
  9. package/dist/{SwitchBotHAPPlatform.js → Platform.HAP.js} +4 -2
  10. package/dist/Platform.HAP.js.map +1 -0
  11. package/dist/{SwitchBotMatterPlatform.d.ts → Platform.Matter.d.ts} +3 -2
  12. package/dist/Platform.Matter.d.ts.map +1 -0
  13. package/dist/{SwitchBotMatterPlatform.js → Platform.Matter.js} +24 -1
  14. package/dist/Platform.Matter.js.map +1 -0
  15. package/dist/homebridge-ui/endpoints/config.d.ts.map +1 -1
  16. package/dist/homebridge-ui/endpoints/config.js +9 -2
  17. package/dist/homebridge-ui/endpoints/config.js.map +1 -1
  18. package/dist/homebridge-ui/homebridge-ui/endpoints/config.js +9 -2
  19. package/dist/homebridge-ui/homebridge-ui/endpoints/config.js.map +1 -1
  20. package/dist/homebridge-ui/homebridge-ui/utils/config-parser.js +8 -0
  21. package/dist/homebridge-ui/homebridge-ui/utils/config-parser.js.map +1 -1
  22. package/dist/homebridge-ui/homebridge-ui/utils/v4-detection.js +23 -0
  23. package/dist/homebridge-ui/homebridge-ui/utils/v4-detection.js.map +1 -0
  24. package/dist/homebridge-ui/public/css/styles.css +62 -2
  25. package/dist/homebridge-ui/public/index.html +61 -6
  26. package/dist/homebridge-ui/public/js/advanced-settings.d.ts.map +1 -1
  27. package/dist/homebridge-ui/public/js/advanced-settings.js +4 -3
  28. package/dist/homebridge-ui/public/js/advanced-settings.js.map +1 -1
  29. package/dist/homebridge-ui/public/js/advanced-settings.ts +4 -3
  30. package/dist/homebridge-ui/public/js/api.d.ts +11 -0
  31. package/dist/homebridge-ui/public/js/api.d.ts.map +1 -1
  32. package/dist/homebridge-ui/public/js/api.js +44 -1
  33. package/dist/homebridge-ui/public/js/api.js.map +1 -1
  34. package/dist/homebridge-ui/public/js/api.ts +46 -1
  35. package/dist/homebridge-ui/public/js/app.js +241 -182
  36. package/dist/homebridge-ui/public/js/app.js.map +4 -4
  37. package/dist/homebridge-ui/public/js/app.ts +47 -4
  38. package/dist/homebridge-ui/public/js/credentials.d.ts.map +1 -1
  39. package/dist/homebridge-ui/public/js/credentials.js +11 -5
  40. package/dist/homebridge-ui/public/js/credentials.js.map +1 -1
  41. package/dist/homebridge-ui/public/js/credentials.ts +11 -5
  42. package/dist/homebridge-ui/public/js/discovery.d.ts.map +1 -1
  43. package/dist/homebridge-ui/public/js/discovery.js +0 -36
  44. package/dist/homebridge-ui/public/js/discovery.js.map +1 -1
  45. package/dist/homebridge-ui/public/js/discovery.ts +0 -37
  46. package/dist/homebridge-ui/public/js/legacy-view.d.ts +3 -0
  47. package/dist/homebridge-ui/public/js/legacy-view.d.ts.map +1 -0
  48. package/dist/homebridge-ui/public/js/legacy-view.js +152 -0
  49. package/dist/homebridge-ui/public/js/legacy-view.js.map +1 -0
  50. package/dist/homebridge-ui/public/js/legacy-view.ts +107 -0
  51. package/dist/homebridge-ui/public/js/render.d.ts.map +1 -1
  52. package/dist/homebridge-ui/public/js/render.js +38 -31
  53. package/dist/homebridge-ui/public/js/render.js.map +1 -1
  54. package/dist/homebridge-ui/public/js/render.ts +39 -34
  55. package/dist/homebridge-ui/public/js/toast.d.ts.map +1 -1
  56. package/dist/homebridge-ui/public/js/toast.js +2 -3
  57. package/dist/homebridge-ui/public/js/toast.js.map +1 -1
  58. package/dist/homebridge-ui/public/js/toast.ts +2 -3
  59. package/dist/homebridge-ui/utils/config-parser.d.ts +8 -0
  60. package/dist/homebridge-ui/utils/config-parser.d.ts.map +1 -1
  61. package/dist/homebridge-ui/utils/config-parser.js +8 -0
  62. package/dist/homebridge-ui/utils/config-parser.js.map +1 -1
  63. package/dist/homebridge-ui/utils/v4-detection.d.ts +12 -0
  64. package/dist/homebridge-ui/utils/v4-detection.d.ts.map +1 -0
  65. package/dist/homebridge-ui/utils/v4-detection.js +23 -0
  66. package/dist/homebridge-ui/utils/v4-detection.js.map +1 -0
  67. package/dist/index.js +2 -2
  68. package/dist/index.js.map +1 -1
  69. package/docs/assets/icons.js +1 -1
  70. package/docs/assets/icons.svg +1 -1
  71. package/docs/assets/main.js +3 -3
  72. package/docs/variables/default.html +1 -1
  73. package/package.json +12 -18
  74. package/scripts/build-ui.js +30 -0
  75. package/src/{SwitchBotHAPPlatform.ts → Platform.HAP.ts} +5 -2
  76. package/src/{SwitchBotMatterPlatform.ts → Platform.Matter.ts} +28 -1
  77. package/src/homebridge-ui/endpoints/config.ts +10 -2
  78. package/src/homebridge-ui/public/css/styles.css +62 -2
  79. package/src/homebridge-ui/public/index.html +59 -4
  80. package/src/homebridge-ui/public/js/advanced-settings.ts +4 -3
  81. package/src/homebridge-ui/public/js/api.ts +46 -1
  82. package/src/homebridge-ui/public/js/app.ts +47 -4
  83. package/src/homebridge-ui/public/js/credentials.ts +11 -5
  84. package/src/homebridge-ui/public/js/discovery.ts +0 -37
  85. package/src/homebridge-ui/public/js/legacy-view.js +152 -0
  86. package/src/homebridge-ui/public/js/legacy-view.ts +107 -0
  87. package/src/homebridge-ui/public/js/render.ts +39 -34
  88. package/src/homebridge-ui/public/js/toast.ts +2 -3
  89. package/src/homebridge-ui/utils/config-parser.ts +9 -0
  90. package/src/homebridge-ui/utils/v4-detection.ts +23 -0
  91. package/src/index.ts +2 -2
  92. package/test/homebridge-ui/config-parser.spec.ts +61 -0
  93. package/test/platform/accessory-restore.spec.ts +2 -2
  94. package/test/platform/matter-childbridge.spec.ts +1 -1
  95. package/test/platform/matter-integration.spec.ts +51 -1
  96. package/test/platform/platform-edge.spec.ts +2 -2
  97. package/test/platform/platform.integration.spec.ts +1 -1
  98. package/dist/SwitchBotHAPPlatform.d.ts.map +0 -1
  99. package/dist/SwitchBotHAPPlatform.js.map +0 -1
  100. package/dist/SwitchBotMatterPlatform.d.ts.map +0 -1
  101. package/dist/SwitchBotMatterPlatform.js.map +0 -1
@@ -1,3 +1,66 @@
1
+ # Matter and HomeKit Integration
2
+
3
+ ## Matter Implementation
4
+ This plugin supports dual-mode device registration:
5
+ - **HAP mode** (HomeKit Accessory Protocol): Base implementation via `SwitchBotPlatform` class
6
+ - **Matter mode**: Extended via `SwitchBotMatterPlatform` class that overrides device registration and processing
7
+
8
+ When Homebridge's Matter API is available, `SwitchBotMatterPlatform` registers accessories with both HAP and Matter simultaneously. If the Matter API is not available, the plugin transparently falls back to HAP-only mode.
9
+
10
+ ## Matter Device Type Mapping
11
+ All Matter device types use `api.matter.deviceTypes.*` objects from the homebridge-matter API. Device mapping aligns with the authoritative [homebridge-matter wiki](https://github.com/homebridge-plugins/homebridge-matter/wiki/Introduction) and Section 7 Sensors for sensor mapping.
12
+
13
+ | Device Type | HAP Service | Matter DeviceType | Matter Clusters |
14
+ |---------------------|-----------------------------|-----------------------|-------------------------|
15
+ | Bot | Switch | `OnOffSwitch` | onOff |
16
+ | Curtain | WindowCovering | `WindowCovering` | windowCovering |
17
+ | Contact Sensor | ContactSensor | `ContactSensor` | contactSensor |
18
+ | Motion Sensor | MotionSensor | `OccupancySensor` | occupancySensing |
19
+ | Meter/Hygrometer | HumiditySensor, TemperatureSensor | `TemperatureSensor`, `HumiditySensor` | temperatureMeasurement, relativeHumidityMeasurement |
20
+ | Lock | LockMechanism | `DoorLock` | doorLock |
21
+ | Light/Fan/Outlet | Lightbulb, Fan, Outlet | `DimmableLight`, `OnOffLight`, `Fan` | onOff, levelControl |
22
+ | IR Devices | Various (see IR mapping) | (varies) | (varies) |
23
+
24
+ ## Authoritative Matter References
25
+
26
+ 1. https://matter-js.github.io/docs/index.html
27
+ 2. https://github.com/homebridge-plugins/homebridge-matter: Official Homebridge Matter plugin repository with extensive documentation and examples
28
+ - For all Matter cluster, attribute, and device type specifications, use the official homebridge-matter wiki:
29
+ - [Introduction](https://github.com/homebridge-plugins/homebridge-matter/wiki/Introduction)
30
+ - [Core Concepts](https://github.com/homebridge-plugins/homebridge-matter/wiki/Core-Concepts)
31
+ - [Getting Started](https://github.com/homebridge-plugins/homebridge-matter/wiki/Getting-Started)
32
+ - [State Management](https://github.com/homebridge-plugins/homebridge-matter/wiki/State-Management)
33
+ - [Monitoring External Changes](https://github.com/homebridge-plugins/homebridge-matter/wiki/Monitoring-External-Changes)
34
+ - [Best Practices](https://github.com/homebridge-plugins/homebridge-matter/wiki/Best-Practices)
35
+ - [Advanced Patterns](https://github.com/homebridge-plugins/homebridge-matter/wiki/Advanced-Patterns)
36
+ - [API Reference](https://github.com/homebridge-plugins/homebridge-matter/wiki/API-Reference)
37
+ - [Matter Types](https://github.com/homebridge-plugins/homebridge-matter/wiki/Matter-Types)
38
+ - [Value Conversions](https://github.com/homebridge-plugins/homebridge-matter/wiki/Value-Conversions)
39
+ - **Device References:**
40
+ - [Lighting Devices (§4)](https://github.com/homebridge-plugins/homebridge-matter/wiki/Section-4-Lighting) — DimmableLight, OnOffLight
41
+ - [Switches (§6)](https://github.com/homebridge-plugins/homebridge-matter/wiki/Section-6-Switches) — OnOffSwitch
42
+ - [Sensors (§7)](https://github.com/homebridge-plugins/homebridge-matter/wiki/Section-7-Sensors) — OccupancySensor, ContactSensor
43
+ - [Closure Devices (§8)](https://github.com/homebridge-plugins/homebridge-matter/wiki/Section-8-Closure) — WindowCovering
44
+
45
+ # Changelog Format Requirements
46
+
47
+ When generating a changelog release entry, always use this exact structure:
48
+
49
+ 1. Release header with compare URL using `compare/tag/vX.Y.Z`:
50
+
51
+ ```md
52
+ ## [X.Y.Z](https://github.com/OpenWonderLabs/homebridge-switchbot/compare/tag/vX.Y.Z) (YYYY-MM-DD)
53
+ ```
54
+
55
+ 2. Standard sections as needed (`### Bug Fixes`, `### Enhancements`, `### Documentation`, etc.).
56
+
57
+ 3. End each release entry with a full changelog comparison URL to the previous version:
58
+
59
+ ```md
60
+ **Full Changelog**: https://github.com/OpenWonderLabs/homebridge-switchbot/compare/vX.Y.(Z-1)...vX.Y.Z
61
+ ```
62
+
63
+ Do not omit either URL line when creating a new release entry.
1
64
  # Homebridge SwitchBot Plugin Development Guide
2
65
 
3
66
  Always reference these instructions first and fallback to search or bash commands only when you encounter unexpected information that does not match the info here.
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env node
2
+
3
+ const child_process = require('node:child_process')
4
+ const fs = require('node:fs')
5
+
6
+ const changelogPath = 'CHANGELOG.md'
7
+
8
+ function normalizeChangelogHeader(content) {
9
+ const headerStart = content.indexOf('# Changelog\n')
10
+ if (headerStart <= 0) {
11
+ return content
12
+ }
13
+
14
+ const nextSectionStart = content.indexOf('\n## ', headerStart + 1)
15
+ const headerBlock = (nextSectionStart === -1 ? content.slice(headerStart) : content.slice(headerStart, nextSectionStart)).trim()
16
+ const prependedEntries = content.slice(0, headerStart).trim()
17
+ const remainingEntries = (nextSectionStart === -1 ? '' : content.slice(nextSectionStart)).trim()
18
+
19
+ return [headerBlock, prependedEntries, remainingEntries].filter(Boolean).join('\n\n') + '\n'
20
+ }
21
+
22
+ function getRepoUrl() {
23
+ try {
24
+ const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'))
25
+ let url = (typeof pkg.repository === 'string' ? pkg.repository : pkg.repository?.url) || ''
26
+ url = url.replace(/^git\+/, '').replace(/\.git$/, '')
27
+ if (url.startsWith('https://')) return url
28
+ } catch {}
29
+ try {
30
+ const remote = child_process.execSync('git remote get-url origin').toString().trim()
31
+ return remote.replace(/^git\+/, '').replace(/\.git$/, '').replace(/^git@github\.com:/, 'https://github.com/')
32
+ } catch {}
33
+ return ''
34
+ }
35
+
36
+ function getPreviousTag() {
37
+ try {
38
+ const tags = child_process
39
+ .execSync('git tag --sort=-version:refname')
40
+ .toString()
41
+ .trim()
42
+ .split('\n')
43
+ .filter(Boolean)
44
+ return tags[0] || ''
45
+ } catch {
46
+ return ''
47
+ }
48
+ }
49
+
50
+ function appendFullChangelogLink(content, currentTag, repoUrl) {
51
+ const prefix = `**Full Changelog**: ${repoUrl}/compare/`
52
+ // Find the bounds of the first release section (the newly generated one)
53
+ const firstSectionStart = content.indexOf('\n## ')
54
+ if (firstSectionStart === -1) return content
55
+ const firstSectionEnd = content.indexOf('\n## ', firstSectionStart + 1)
56
+ const firstSection = firstSectionEnd === -1 ? content.slice(firstSectionStart) : content.slice(firstSectionStart, firstSectionEnd)
57
+
58
+ // Skip if a Full Changelog link is already present in that section
59
+ if (firstSection.includes(prefix)) return content
60
+
61
+ const prevTag = getPreviousTag()
62
+ if (!prevTag) return content
63
+
64
+ const link = `**Full Changelog**: ${repoUrl}/compare/${prevTag}...${currentTag}`
65
+ if (firstSectionEnd === -1) {
66
+ return content.trimEnd() + '\n\n' + link + '\n'
67
+ }
68
+ return content.slice(0, firstSectionEnd).trimEnd() + '\n\n' + link + '\n' + content.slice(firstSectionEnd)
69
+ }
70
+
71
+ exports.preCommit = (props) => {
72
+ if (!fs.existsSync(changelogPath)) {
73
+ return
74
+ }
75
+
76
+ let content = fs.readFileSync(changelogPath, 'utf8')
77
+ content = normalizeChangelogHeader(content)
78
+
79
+ const repoUrl = getRepoUrl()
80
+ const currentTag = props?.tag || ''
81
+ if (repoUrl && currentTag) {
82
+ content = appendFullChangelogLink(content, currentTag, repoUrl)
83
+ }
84
+
85
+ fs.writeFileSync(changelogPath, content)
86
+ }
@@ -0,0 +1,201 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Standalone ESM implementation — self-contained so this file can be copied
5
+ * into other repos without pulling any other files.
6
+ */
7
+
8
+ import assert from 'node:assert'
9
+ import child_process from 'node:child_process'
10
+ import fs from 'node:fs'
11
+ import process from 'node:process'
12
+ // Minimal, in-file semver utilities used by this script.
13
+ // This avoids an external dependency so the script can run in environments
14
+ // where `semver` isn't installed.
15
+ const semver = {
16
+ // Very small validation: match major.minor.patch with optional prerelease/build
17
+ valid(v) {
18
+ return /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-.]+)?(?:\+[0-9A-Za-z-.]+)?$/.test(v)
19
+ },
20
+ // Return prerelease identifiers array or null
21
+ prerelease(v) {
22
+ const m = v.match(/^\d+\.\d+\.\d+-(.+)$/)
23
+ return m ? m[1].split('.') : null
24
+ },
25
+ // Compare two semver strings in reverse order (for rcompare use)
26
+ rcompare(a, b) {
27
+ // Use standard semver precedence rules for major.minor.patch; ignore prerelease complexity except that prerelease is lower than release
28
+ const pa = a.split('-')
29
+ const pb = b.split('-')
30
+ const aCore = pa[0].split('.').map(Number)
31
+ const bCore = pb[0].split('.').map(Number)
32
+ for (let i = 0; i < 3; i++) {
33
+ if (aCore[i] > bCore[i]) return -1
34
+ if (aCore[i] < bCore[i]) return 1
35
+ }
36
+ // equal core; releases (no prerelease) are greater than prerelease
37
+ if (pa.length === 1 && pb.length > 1) return -1
38
+ if (pa.length > 1 && pb.length === 1) return 1
39
+ // both same prerelease status: compare as strings
40
+ if (a > b) return -1
41
+ if (a < b) return 1
42
+ return 0
43
+ },
44
+ // Increment version by 'major'|'minor'|'patch'
45
+ inc(v, level) {
46
+ if (!this.valid(v)) throw new Error(`Invalid semver passed to inc: ${v}`)
47
+ // strip prerelease/build
48
+ const core = v.split('-')[0].split('+')[0]
49
+ const parts = core.split('.').map(Number)
50
+ switch (level) {
51
+ case 'major':
52
+ parts[0] += 1
53
+ parts[1] = 0
54
+ parts[2] = 0
55
+ break
56
+ case 'minor':
57
+ parts[1] += 1
58
+ parts[2] = 0
59
+ break
60
+ case 'patch':
61
+ default:
62
+ parts[2] += 1
63
+ break
64
+ }
65
+ return parts.join('.')
66
+ }
67
+ }
68
+
69
+ const BRANCH_VERSION_PATTERN = /^([A-Z]+)-(\d+\.\d+\.\d+)$/i
70
+
71
+ // Load package.json
72
+ const packageJSON = JSON.parse(fs.readFileSync('package.json', 'utf8'))
73
+
74
+ const refArgument = process.argv[2]
75
+ const tagArgument = process.argv[3] || 'latest'
76
+
77
+ if (!refArgument) {
78
+ console.error('ref argument is missing')
79
+ console.error('Usage: npm-version-script-esm.js <ref> [tag]')
80
+ process.exit(1)
81
+ }
82
+
83
+ /**
84
+ * Queries the NPM registry for the latest version for the provided base version and tag.
85
+ * If the tag is latest, then the base version is returned if it exists. For other tags, the latest
86
+ * version found for that base version and tag is returned.
87
+ * @param baseVersion The base version to query for, e.g. 2.0.0
88
+ * @param tag The tag to query for, e.g. beta or latest
89
+ * @returns {string} Returns the version, or '' if not found
90
+ */
91
+ function getTagVersionFromNpm(baseVersion, tag) {
92
+ try {
93
+ return JSON.parse(child_process.execSync(`npm info ${packageJSON.name} versions --json`).toString('utf8').trim())
94
+ .filter(v => (tag === 'latest' ? v === baseVersion : v.startsWith(`${baseVersion}-${tag}.`)))
95
+ .reduce((_, current) => current, '')
96
+ } catch (e) {
97
+ console.error(`Failed to query the npm registry for the latest version for tag: ${tag}`, e)
98
+ // throw e;
99
+ return ''
100
+ }
101
+ }
102
+
103
+ function getLatestStableVersionFromNpm() {
104
+ try {
105
+ const versions = JSON.parse(child_process.execSync(`npm info ${packageJSON.name} versions --json`).toString('utf8').trim())
106
+ if (!Array.isArray(versions) || versions.length === 0) return ''
107
+
108
+ // Only consider stable semver versions (no prerelease)
109
+ const stable = versions.filter(v => semver.valid(v) && !semver.prerelease(v))
110
+ if (stable.length === 0) return ''
111
+
112
+ // Sort descending and return the highest
113
+ const sorted = stable.sort(semver.rcompare)
114
+ return sorted[0]
115
+ } catch (e) {
116
+ console.error('Failed to query the npm registry for published versions', e)
117
+ return ''
118
+ }
119
+ }
120
+
121
+ function desiredTargetVersion(ref) {
122
+ // ref is a GitHub action ref string
123
+ if (ref.startsWith('refs/pull/')) {
124
+ throw new Error('The version script was executed inside a PR!')
125
+ }
126
+
127
+ assert(ref.startsWith('refs/heads/'))
128
+ const branchName = ref.slice('refs/heads/'.length)
129
+
130
+ const results = branchName.match(BRANCH_VERSION_PATTERN)
131
+ if (results !== null) {
132
+ if (results[1].toLowerCase() !== tagArgument.toLowerCase()) {
133
+ console.warn(`The base branch name (${results[1]}) differs from the tag name ${tagArgument}`)
134
+ }
135
+
136
+ return results[2]
137
+ }
138
+
139
+ if (branchName === 'latest') {
140
+ // For latest branch, start with package.json version as base
141
+ return packageJSON.version
142
+ }
143
+
144
+ throw new Error(`Malformed branch name for ref: ${ref}. Must be beta-x.x.x, alpha-x.x.x, or 'latest'`)
145
+ }
146
+
147
+ function bumpVersion(baseVersion, level) {
148
+ const normalizedLevel = (level || 'patch').toLowerCase()
149
+ if (!semver.valid(baseVersion)) {
150
+ throw new Error(`Invalid base version passed to bumpVersion: ${baseVersion}`)
151
+ }
152
+
153
+ switch (normalizedLevel) {
154
+ case 'major':
155
+ return semver.inc(baseVersion, 'major')
156
+ case 'minor':
157
+ return semver.inc(baseVersion, 'minor')
158
+ case 'patch':
159
+ default:
160
+ return semver.inc(baseVersion, 'patch')
161
+ }
162
+ }
163
+
164
+ const baseVersion = desiredTargetVersion(refArgument)
165
+ let publishTag = baseVersion
166
+
167
+ if (refArgument.includes('latest')) {
168
+ // For latest branch, determine next version based on npm and bump patch.
169
+ const latestPublishedStable = getLatestStableVersionFromNpm()
170
+ if (latestPublishedStable) {
171
+ console.warn(`Latest published stable version is ${latestPublishedStable}; bumping patch`)
172
+ publishTag = bumpVersion(latestPublishedStable, 'patch')
173
+ } else {
174
+ console.warn('No published stable versions found; bumping package.json version patch')
175
+ publishTag = bumpVersion(baseVersion, 'patch')
176
+ }
177
+ } else {
178
+ // For alpha/beta, query npm for latest
179
+ const latestReleasedVersion = getTagVersionFromNpm(baseVersion, tagArgument)
180
+ if (latestReleasedVersion) {
181
+ console.warn(`Latest published version for ${baseVersion} with tag ${tagArgument} is ${latestReleasedVersion}`)
182
+ publishTag = latestReleasedVersion
183
+ } else {
184
+ console.warn(`No published versions for ${baseVersion} with tag ${tagArgument}`)
185
+ }
186
+ }
187
+
188
+ if (packageJSON.version !== publishTag) {
189
+ console.warn(`Updating version in package.json from ${packageJSON.version} to ${publishTag}`)
190
+ packageJSON.version = publishTag
191
+ fs.writeFileSync('package.json', JSON.stringify(packageJSON, null, 2))
192
+
193
+ // perform the same change to the package-lock.json
194
+ const packageLockJSON = JSON.parse(fs.readFileSync('package-lock.json', 'utf8'))
195
+ packageLockJSON.version = publishTag
196
+ fs.writeFileSync('package-lock.json', JSON.stringify(packageLockJSON, null, 2))
197
+ } else {
198
+ console.warn(`Version in package.json is already ${packageJSON.version}`)
199
+ }
200
+
201
+ console.log(publishTag)
@@ -0,0 +1,118 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'node:fs'
4
+ import https from 'node:https'
5
+ import process from 'node:process'
6
+
7
+ const allowed = new Set(['major', 'minor', 'patch'])
8
+
9
+ function setOutput(name, value) {
10
+ if (process.env.GITHUB_OUTPUT) {
11
+ fs.appendFileSync(process.env.GITHUB_OUTPUT, `${name}=${value}\n`)
12
+ }
13
+ console.log(`${name}=${value}`)
14
+ }
15
+
16
+ function resolveFromLabels(labels) {
17
+ const majorLabels = new Set(['major', 'semver:major', 'release:major', 'breaking', 'breaking-change'])
18
+ const minorLabels = new Set(['minor', 'semver:minor', 'release:minor', 'feature'])
19
+ const patchLabels = new Set(['patch', 'semver:patch', 'release:patch', 'fix'])
20
+
21
+ if (labels.some(label => majorLabels.has(label))) return 'major'
22
+ if (labels.some(label => minorLabels.has(label))) return 'minor'
23
+ if (labels.some(label => patchLabels.has(label))) return 'patch'
24
+ return 'patch'
25
+ }
26
+
27
+ function listPrsForCommit(owner, repo, sha, token) {
28
+ return new Promise((resolve, reject) => {
29
+ const path = `/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/commits/${sha}/pulls`
30
+ const req = https.request(
31
+ {
32
+ hostname: 'api.github.com',
33
+ method: 'GET',
34
+ path,
35
+ headers: {
36
+ Accept: 'application/vnd.github+json',
37
+ Authorization: `Bearer ${token}`,
38
+ 'User-Agent': 'homebridge-resolve-version-bump',
39
+ 'X-GitHub-Api-Version': '2022-11-28',
40
+ },
41
+ },
42
+ (res) => {
43
+ let body = ''
44
+ res.setEncoding('utf8')
45
+ res.on('data', (chunk) => {
46
+ body += chunk
47
+ })
48
+ res.on('end', () => {
49
+ if (res.statusCode < 200 || res.statusCode >= 300) {
50
+ reject(new Error(`GitHub API request failed (${res.statusCode}): ${body}`))
51
+ return
52
+ }
53
+ try {
54
+ const parsed = JSON.parse(body)
55
+ resolve(Array.isArray(parsed) ? parsed : [])
56
+ } catch (error) {
57
+ reject(new Error(`Failed to parse GitHub API response: ${error.message}`))
58
+ }
59
+ })
60
+ },
61
+ )
62
+
63
+ req.on('error', reject)
64
+ req.end()
65
+ })
66
+ }
67
+
68
+ async function main() {
69
+ const manualInput = (process.argv[2] || '').trim().toLowerCase()
70
+ if (manualInput) {
71
+ if (!allowed.has(manualInput)) {
72
+ console.error(`Invalid version bump '${manualInput}'. Use major, minor, or patch.`)
73
+ process.exit(1)
74
+ }
75
+ console.error(`Using manual version bump input: ${manualInput}`)
76
+ setOutput('version_bump', manualInput)
77
+ return
78
+ }
79
+
80
+ const repository = process.env.GITHUB_REPOSITORY || ''
81
+ const sha = process.env.GITHUB_SHA || ''
82
+ const token = process.env.GITHUB_TOKEN || ''
83
+
84
+ if (!repository || !sha || !token || !repository.includes('/')) {
85
+ console.error('Missing GITHUB_REPOSITORY, GITHUB_SHA, or GITHUB_TOKEN. Defaulting to patch.')
86
+ setOutput('version_bump', 'patch')
87
+ return
88
+ }
89
+
90
+ const [owner, repo] = repository.split('/')
91
+
92
+ try {
93
+ const prs = await listPrsForCommit(owner, repo, sha, token)
94
+ const pr = prs.find((candidate) => candidate.merged_at) || prs[0]
95
+
96
+ if (!pr) {
97
+ console.error('No PR associated with commit; defaulting to patch.')
98
+ setOutput('version_bump', 'patch')
99
+ return
100
+ }
101
+
102
+ const labels = (pr.labels || [])
103
+ .map((label) => (typeof label === 'string' ? label : label.name || ''))
104
+ .map((label) => label.toLowerCase())
105
+
106
+ const bump = resolveFromLabels(labels)
107
+ console.error(`Resolved bump '${bump}' from PR #${pr.number} labels: ${labels.join(', ') || '(none)'}`)
108
+ setOutput('version_bump', bump)
109
+ } catch (error) {
110
+ console.error(`Failed to inspect PR labels, defaulting to patch: ${error.message}`)
111
+ setOutput('version_bump', 'patch')
112
+ }
113
+ }
114
+
115
+ main().catch((error) => {
116
+ console.error(error)
117
+ process.exit(1)
118
+ })
@@ -7,13 +7,54 @@ on:
7
7
  - 'beta'
8
8
  - 'latest'
9
9
  workflow_dispatch:
10
+ inputs:
11
+ release_type:
12
+ description: Release channel
13
+ required: true
14
+ type: choice
15
+ options:
16
+ - latest
17
+ - beta
18
+ - alpha
19
+ default: latest
20
+ is_esm:
21
+ description: Package is ESM
22
+ required: true
23
+ type: boolean
24
+ default: true
25
+ version_override:
26
+ description: Exact version (for example 2.1.0). Leave empty for auto bump.
27
+ required: false
28
+ type: string
29
+ version_bump:
30
+ description: Auto bump type when version_override is empty
31
+ required: false
32
+ type: choice
33
+ options:
34
+ - patch
35
+ - minor
36
+ - major
37
+ default: patch
38
+ max_attempts:
39
+ description: Max retries for prerelease collision handling
40
+ required: false
41
+ type: number
42
+ default: 5
43
+
44
+ # Cancel any in-progress workflow for the same branch. A new push should
45
+ # supersede the previous run rather than racing it to publish.
46
+ concurrency:
47
+ group: release-${{ github.ref }}
48
+ cancel-in-progress: true
10
49
 
11
50
  jobs:
12
- # ─── BETA RELEASE (branches starting with "beta") ────────────────────────────
51
+ # ─── QUALITY GATE (all branches) ────────────────────────────────────────────
52
+ # Build and test run for EVERY push, regardless of branch. Both the beta
53
+ # and stable release paths depend on these passing before anything is
54
+ # published or version-bumped.
13
55
 
14
56
  # 1️⃣ Build and test
15
57
  build_and_test:
16
- if: startsWith(github.ref_name, 'beta')
17
58
  uses: homebridge/.github/.github/workflows/nodejs-build-and-test.yml@latest
18
59
  with:
19
60
  enable_coverage: false
@@ -22,10 +63,12 @@ jobs:
22
63
 
23
64
  # 2️⃣ Lint
24
65
  lint:
25
- if: startsWith(github.ref_name, 'beta')
26
66
  needs: build_and_test
27
67
  uses: homebridge/.github/.github/workflows/eslint.yml@latest
28
68
 
69
+
70
+ # ─── BETA RELEASE (branches starting with "beta") ────────────────────────────
71
+
29
72
  # 3️⃣ Publish beta to NPM (OIDC — id-token required here and in the reusable workflow)
30
73
  beta-publish:
31
74
  if: startsWith(github.ref_name, 'beta')
@@ -54,23 +97,26 @@ jobs:
54
97
 
55
98
  # ─── STABLE RELEASE (branch: "latest") ───────────────────────────────────────
56
99
 
57
- # 1️⃣ Determine release type, ESM status, and branch name
100
+ # 3️⃣ Determine release type, ESM status, and branch name
58
101
  determine-release-type:
59
102
  if: github.ref_name == 'latest'
103
+ needs: lint
60
104
  uses: homebridge/.github/.github/workflows/determine-release-type.yml@latest
61
105
  with:
62
106
  ref_name: ${{ github.ref_name }}
63
107
 
64
- # 2️⃣ Update version and changelog using the scripts
108
+ # 4️⃣ Update version and changelog using the scripts
65
109
  update-version:
66
110
  if: github.ref_name == 'latest'
67
111
  needs: determine-release-type
68
112
  uses: homebridge/.github/.github/workflows/update-version.yml@latest
69
113
  with:
70
- release_type: ${{ needs.determine-release-type.outputs.release_type }}
71
- is_esm: ${{ needs.determine-release-type.outputs.is_esm == 'true' }}
114
+ release_type: ${{ needs.determine-release-type.outputs.release_type || inputs.release_type }}
115
+ is_esm: ${{ needs.determine-release-type.outputs.is_esm == 'true' || inputs.is_esm == 'true' }}
116
+ version_override: ${{ inputs.version_override }}
117
+ version_bump: ${{ inputs.version_bump }}
72
118
 
73
- # 3️⃣ Publish to NPM and create GitHub release (OIDC — id-token required here and in the reusable workflow)
119
+ # 5️⃣ Publish to NPM and create GitHub release (OIDC — id-token required here and in the reusable workflow)
74
120
  publish-release:
75
121
  if: github.ref_name == 'latest'
76
122
  needs: [determine-release-type, update-version]
@@ -82,10 +128,13 @@ jobs:
82
128
  release_type: ${{ needs.determine-release-type.outputs.release_type }}
83
129
  version: ${{ needs.update-version.outputs.version }}
84
130
  is_esm: ${{ needs.determine-release-type.outputs.is_esm == 'true' }}
131
+ version_override: ${{ inputs.version_override || '' }}
132
+ version_bump: ${{ inputs.version_bump || 'patch' }}
133
+ max_attempts: ${{ inputs.max_attempts || 5 }}
85
134
  secrets:
86
135
  NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
87
136
 
88
- # 4️⃣ Promote branch if this is a prerelease (alpha/beta)
137
+ # 6️⃣ Promote branch if this is a prerelease (alpha/beta)
89
138
  promote-branch:
90
139
  if: ${{ github.ref_name == 'latest' && needs.determine-release-type.outputs.release_type != 'latest' && needs.determine-release-type.outputs.release_type != 'skip' }}
91
140
  needs: [determine-release-type, publish-release]
@@ -93,19 +142,19 @@ jobs:
93
142
  with:
94
143
  branch_name: ${{ needs.determine-release-type.outputs.branch_name }}
95
144
  release_type: ${{ needs.determine-release-type.outputs.release_type }}
96
- is_esm: ${{ needs.determine-release-type.outputs.is_esm == 'true' }}
145
+ is_esm: ${{ needs.determine-release-type.outputs.is_esm == 'true' || inputs.is_esm == 'true' }}
97
146
 
98
- # 5️⃣ Notify if any stable-release job fails
147
+ # 7️⃣ Notify if any job fails
99
148
  workflow-failure:
100
149
  if: failure()
101
- needs: [determine-release-type, update-version, publish-release, promote-branch]
150
+ needs: [build_and_test, lint, determine-release-type, update-version, publish-release, promote-branch]
102
151
  uses: homebridge/.github/.github/workflows/report-failure.yml@latest
103
152
  with:
104
153
  workflow_name: ${{ github.workflow }}
105
154
  job_name: ${{ github.job }}
106
155
  run_url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
107
156
 
108
- # 6️⃣ Post to Discord (Beta + Stable)
157
+ # 8️⃣ Post to Discord (Beta + Stable)
109
158
  discord:
110
159
  name: Release Notifications
111
160
  if: ${{ always() && ((startsWith(github.ref_name, 'beta') && needs.beta-publish.result == 'success') || (github.ref_name == 'latest' && needs.publish-release.result == 'success')) }}
package/CHANGELOG.md CHANGED
@@ -1,3 +1,21 @@
1
+ ## [5.0.2](https://github.com/OpenWonderLabs/homebridge-switchbot/compare/v4.3.14...v5.0.2) (2026-04-26)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * preserve LockTargetState during in-flight lock/unlock commands ([#1345](https://github.com/OpenWonderLabs/homebridge-switchbot/issues/1345)) ([cf6d7ae](https://github.com/OpenWonderLabs/homebridge-switchbot/commit/cf6d7aeb7d9d6a3ea4c26fb6abaa0ea39fb08d8e))
7
+
8
+
9
+
10
+ ## [5.0.1](https://github.com/OpenWonderLabs/homebridge-switchbot/compare/v4.3.14...v5.0.1) (2026-04-17)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * preserve LockTargetState during in-flight lock/unlock commands ([#1345](https://github.com/OpenWonderLabs/homebridge-switchbot/issues/1345)) ([cf6d7ae](https://github.com/OpenWonderLabs/homebridge-switchbot/commit/cf6d7aeb7d9d6a3ea4c26fb6abaa0ea39fb08d8e))
16
+
17
+
18
+
1
19
  # Changelog
2
20
 
3
21
  All notable changes to this project will be documented in this file. This project uses [Semantic Versioning](https://semver.org/)
@@ -1,5 +1,5 @@
1
- import type { SwitchBotPluginConfig } from './settings.js';
2
1
  import type { API, Logger, PlatformConfig } from 'homebridge';
2
+ import type { SwitchBotPluginConfig } from './settings.js';
3
3
  /**
4
4
  * Homebridge platform class for SwitchBot HAP (HomeKit Accessory Protocol) integration.
5
5
  * Handles device discovery, registration, polling, and accessory lifecycle for HAP-enabled SwitchBot devices.
@@ -130,4 +130,4 @@ export declare class SwitchBotHAPPlatform {
130
130
  configureMatterAccessory?(accessory: any): void;
131
131
  }
132
132
  export default SwitchBotHAPPlatform;
133
- //# sourceMappingURL=SwitchBotHAPPlatform.d.ts.map
133
+ //# sourceMappingURL=Platform.HAP.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Platform.HAP.d.ts","sourceRoot":"","sources":["../src/Platform.HAP.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAE7D,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAA;AAO1D;;;;;;;;;;;;;;;;;;;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,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;IA8DlC;;;;;;;;;;;;;;;;;;;;OAoBG;YACW,sBAAsB;IAyIpC;;;OAGG;IACH,mBAAmB,IAAI,MAAM;IAI7B;;;OAGG;IACH,mBAAmB,IAAI,IAAI;IAS3B;;;OAGG;IACH,OAAO,CAAC,aAAa;IAWrB;;;;;;OAMG;YACW,qBAAqB;IAiCnC;;;;OAIG;IACH,QAAQ,IAAI,IAAI;IAQhB;;;;;OAKG;IACH,OAAO,CAAC,oBAAoB;IAkI5B;;;;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"}
@@ -208,7 +208,9 @@ export class SwitchBotHAPPlatform {
208
208
  }
209
209
  }
210
210
  if (!accessory) {
211
- accessory = new this.api.platformAccessory(d.name || type, uuid);
211
+ // Use the PlatformAccessory constructor from the HAP API
212
+ const PlatformAccessory = this.api.hap?.PlatformAccessory || this.api.platformAccessory;
213
+ accessory = new PlatformAccessory(d.name || type, uuid);
212
214
  try {
213
215
  accessory.context = accessory.context || {};
214
216
  accessory.context.deviceId = d.id;
@@ -581,4 +583,4 @@ export class SwitchBotHAPPlatform {
581
583
  }
582
584
  }
583
585
  export default SwitchBotHAPPlatform;
584
- //# sourceMappingURL=SwitchBotHAPPlatform.js.map
586
+ //# sourceMappingURL=Platform.HAP.js.map