@switchbot/homebridge-switchbot 5.0.0-beta.39 → 5.0.0-beta.40
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/config.schema.json +18 -0
- package/dist/homebridge-ui/server.js +10 -4
- package/dist/homebridge-ui/server.js.map +1 -1
- package/dist/platform-hap.d.ts.map +1 -1
- package/dist/platform-hap.js +28 -4
- package/dist/platform-hap.js.map +1 -1
- package/dist/platform-matter.d.ts.map +1 -1
- package/dist/platform-matter.js +22 -6
- package/dist/platform-matter.js.map +1 -1
- package/dist/settings.d.ts +18 -0
- package/dist/settings.d.ts.map +1 -1
- package/dist/settings.js.map +1 -1
- package/dist/utils.d.ts +18 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +89 -1
- package/dist/utils.js.map +1 -1
- package/docs/variables/default.html +1 -1
- package/package.json +2 -2
- package/src/homebridge-ui/server.ts +10 -4
- package/src/platform-hap.ts +28 -4
- package/src/platform-matter.ts +22 -6
- package/src/settings.ts +18 -0
- package/src/utils.ts +100 -1
package/src/settings.ts
CHANGED
|
@@ -59,6 +59,24 @@ export interface options {
|
|
|
59
59
|
updateRate?: number
|
|
60
60
|
pushRate?: number
|
|
61
61
|
logging?: string
|
|
62
|
+
/**
|
|
63
|
+
* Maximum number of SwitchBot OpenAPI requests allowed per day.
|
|
64
|
+
* Defaults to 10,000 if not specified.
|
|
65
|
+
*/
|
|
66
|
+
dailyApiLimit?: number
|
|
67
|
+
/**
|
|
68
|
+
* Number of daily API requests reserved for user-initiated commands.
|
|
69
|
+
* When remaining budget falls below this reserve, background polling and discovery
|
|
70
|
+
* are paused until the daily counter resets. Defaults to 1,000.
|
|
71
|
+
*/
|
|
72
|
+
dailyApiReserveForCommands?: number
|
|
73
|
+
/**
|
|
74
|
+
* When true, the plugin will completely stop background polling/discovery
|
|
75
|
+
* once the remaining daily budget reaches the reserve (webhook-only mode).
|
|
76
|
+
* When false, polling continues until the hard daily limit is reached.
|
|
77
|
+
* Default: false.
|
|
78
|
+
*/
|
|
79
|
+
webhookOnlyOnReserve?: boolean
|
|
62
80
|
// Matter platform batch refresh options
|
|
63
81
|
matterBatchRefreshRate?: number
|
|
64
82
|
matterBatchConcurrency?: number
|
package/src/utils.ts
CHANGED
|
@@ -963,11 +963,20 @@ export class ApiRequestTracker {
|
|
|
963
963
|
private date = ''
|
|
964
964
|
private statsFile = ''
|
|
965
965
|
private hourlyTimer?: NodeJS.Timeout
|
|
966
|
+
private midnightTimer?: NodeJS.Timeout
|
|
966
967
|
private log: Logging
|
|
968
|
+
// Daily limits
|
|
969
|
+
private dailyLimit: number
|
|
970
|
+
private reserveForCommands: number
|
|
971
|
+
private lastWarn: Record<string, number> = {}
|
|
972
|
+
private pausePollingAtReserve = false
|
|
967
973
|
|
|
968
|
-
constructor(api: API, log: Logging, pluginName = 'SwitchBot') {
|
|
974
|
+
constructor(api: API, log: Logging, pluginName = 'SwitchBot', limits?: { dailyLimit?: number, reserveForCommands?: number, pausePollingAtReserve?: boolean }) {
|
|
969
975
|
this.log = log
|
|
970
976
|
this.statsFile = join(api.user.storagePath(), `${pluginName.toLowerCase()}-api-stats.json`)
|
|
977
|
+
this.dailyLimit = Math.max(0, Number(limits?.dailyLimit ?? 10000))
|
|
978
|
+
this.reserveForCommands = Math.max(0, Number(limits?.reserveForCommands ?? 1000))
|
|
979
|
+
this.pausePollingAtReserve = Boolean(limits?.pausePollingAtReserve ?? false)
|
|
971
980
|
this.load()
|
|
972
981
|
}
|
|
973
982
|
|
|
@@ -1038,6 +1047,50 @@ export class ApiRequestTracker {
|
|
|
1038
1047
|
this.save()
|
|
1039
1048
|
}
|
|
1040
1049
|
|
|
1050
|
+
/**
|
|
1051
|
+
* Attempt to spend from the daily budget for a request of a given kind.
|
|
1052
|
+
* Kinds: 'command' (user actions), 'poll' (status refresh), 'discovery'.
|
|
1053
|
+
* Returns true if allowed (and increments the counter), false if blocked.
|
|
1054
|
+
*/
|
|
1055
|
+
public trySpend(kind: 'command' | 'poll' | 'discovery', n = 1): boolean {
|
|
1056
|
+
const today = new Date().toISOString().split('T')[0]
|
|
1057
|
+
if (this.date !== today) {
|
|
1058
|
+
// Day rollover
|
|
1059
|
+
this.log.debug?.(`[API Stats] Day rollover: ${this.count} requests on ${this.date}`)
|
|
1060
|
+
this.count = 0
|
|
1061
|
+
this.date = today
|
|
1062
|
+
this.save()
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
const softCap = Math.max(0, this.dailyLimit - this.reserveForCommands)
|
|
1066
|
+
const projected = this.count + n
|
|
1067
|
+
const now = Date.now()
|
|
1068
|
+
const overHardCap = projected > this.dailyLimit
|
|
1069
|
+
const overSoftCap = projected > softCap
|
|
1070
|
+
const shouldRateLimit = (kind === 'command')
|
|
1071
|
+
? overHardCap
|
|
1072
|
+
: (this.pausePollingAtReserve ? overSoftCap : overHardCap)
|
|
1073
|
+
|
|
1074
|
+
if (shouldRateLimit) {
|
|
1075
|
+
const warnKey = kind === 'command' ? 'hardcap' : 'softcap'
|
|
1076
|
+
const last = this.lastWarn[warnKey] ?? 0
|
|
1077
|
+
if (now - last > 10 * 60 * 1000) { // warn at most every 10 minutes
|
|
1078
|
+
if (kind === 'command') {
|
|
1079
|
+
this.log.error?.(`[API Stats] Daily limit (${this.dailyLimit}) reached. Blocking command requests until reset.`)
|
|
1080
|
+
} else {
|
|
1081
|
+
const remainingForCommands = Math.max(0, this.dailyLimit - this.count)
|
|
1082
|
+
this.log.warn?.(`[API Stats] Near daily limit. Pausing ${kind} requests to reserve ~${this.reserveForCommands} calls for commands. Remaining today: ${remainingForCommands}`)
|
|
1083
|
+
}
|
|
1084
|
+
this.lastWarn[warnKey] = now
|
|
1085
|
+
}
|
|
1086
|
+
return false
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
this.count += n
|
|
1090
|
+
this.save()
|
|
1091
|
+
return true
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1041
1094
|
/**
|
|
1042
1095
|
* Start hourly logging of API request count
|
|
1043
1096
|
*/
|
|
@@ -1054,9 +1107,13 @@ export class ApiRequestTracker {
|
|
|
1054
1107
|
this.count = 0
|
|
1055
1108
|
this.date = today
|
|
1056
1109
|
this.save()
|
|
1110
|
+
this.log.info?.('[API Stats] Polling resumed after daily reset')
|
|
1057
1111
|
}
|
|
1058
1112
|
this.log.info?.(`[API Stats] Today (${this.date}): ${this.count} API requests`)
|
|
1059
1113
|
}, 60 * 60 * 1000) // Every hour
|
|
1114
|
+
|
|
1115
|
+
// Schedule an exact UTC midnight rollover log/reset
|
|
1116
|
+
this.scheduleMidnightRollover()
|
|
1060
1117
|
}
|
|
1061
1118
|
|
|
1062
1119
|
/**
|
|
@@ -1067,6 +1124,10 @@ export class ApiRequestTracker {
|
|
|
1067
1124
|
clearInterval(this.hourlyTimer)
|
|
1068
1125
|
this.hourlyTimer = undefined
|
|
1069
1126
|
}
|
|
1127
|
+
if (this.midnightTimer) {
|
|
1128
|
+
clearTimeout(this.midnightTimer)
|
|
1129
|
+
this.midnightTimer = undefined
|
|
1130
|
+
}
|
|
1070
1131
|
}
|
|
1071
1132
|
|
|
1072
1133
|
/**
|
|
@@ -1082,6 +1143,44 @@ export class ApiRequestTracker {
|
|
|
1082
1143
|
public getDate(): string {
|
|
1083
1144
|
return this.date
|
|
1084
1145
|
}
|
|
1146
|
+
|
|
1147
|
+
/** Schedule a precise log/reset at the next UTC midnight */
|
|
1148
|
+
private scheduleMidnightRollover(): void {
|
|
1149
|
+
try {
|
|
1150
|
+
// Clear any previous timer
|
|
1151
|
+
if (this.midnightTimer) {
|
|
1152
|
+
clearTimeout(this.midnightTimer)
|
|
1153
|
+
this.midnightTimer = undefined
|
|
1154
|
+
}
|
|
1155
|
+
const now = new Date()
|
|
1156
|
+
const nextUtcMidnightMs = Date.UTC(
|
|
1157
|
+
now.getUTCFullYear(),
|
|
1158
|
+
now.getUTCMonth(),
|
|
1159
|
+
now.getUTCDate() + 1,
|
|
1160
|
+
0,
|
|
1161
|
+
0,
|
|
1162
|
+
0,
|
|
1163
|
+
0,
|
|
1164
|
+
)
|
|
1165
|
+
const delay = Math.max(1000, nextUtcMidnightMs - now.getTime())
|
|
1166
|
+
this.midnightTimer = setTimeout(() => {
|
|
1167
|
+
try {
|
|
1168
|
+
const today = new Date().toISOString().split('T')[0]
|
|
1169
|
+
if (this.date !== today) {
|
|
1170
|
+
this.log.info?.(`[API Stats] Day rollover - Previous day (${this.date}): ${this.count} API requests`)
|
|
1171
|
+
this.count = 0
|
|
1172
|
+
this.date = today
|
|
1173
|
+
this.save()
|
|
1174
|
+
}
|
|
1175
|
+
// Emit the precise resume line and a fresh today counter line
|
|
1176
|
+
this.log.info?.('[API Stats] Polling resumed after daily reset')
|
|
1177
|
+
this.log.info?.(`[API Stats] Today (${this.date}): ${this.count} API requests`)
|
|
1178
|
+
} catch {}
|
|
1179
|
+
// Reschedule for the next midnight
|
|
1180
|
+
this.scheduleMidnightRollover()
|
|
1181
|
+
}, delay)
|
|
1182
|
+
} catch {}
|
|
1183
|
+
}
|
|
1085
1184
|
}
|
|
1086
1185
|
|
|
1087
1186
|
/**
|