@ecobridge.xyz/devicemanager 3.0.2
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/.gitea/workflows/default_nottags.yaml +66 -0
- package/.gitea/workflows/default_tags.yaml +124 -0
- package/.vscode/launch.json +11 -0
- package/.vscode/settings.json +26 -0
- package/changelog.md +91 -0
- package/dist_ts/00_commitinfo_data.d.ts +8 -0
- package/dist_ts/00_commitinfo_data.js +9 -0
- package/dist_ts/device/device.classes.device.d.ts +205 -0
- package/dist_ts/device/device.classes.device.js +344 -0
- package/dist_ts/devicemanager.classes.devicemanager.d.ts +210 -0
- package/dist_ts/devicemanager.classes.devicemanager.js +835 -0
- package/dist_ts/discovery/discovery.classes.homeassistant.d.ts +101 -0
- package/dist_ts/discovery/discovery.classes.homeassistant.js +313 -0
- package/dist_ts/discovery/discovery.classes.mdns.d.ts +90 -0
- package/dist_ts/discovery/discovery.classes.mdns.js +291 -0
- package/dist_ts/discovery/discovery.classes.networkscanner.d.ts +64 -0
- package/dist_ts/discovery/discovery.classes.networkscanner.js +535 -0
- package/dist_ts/discovery/discovery.classes.ssdp.d.ts +112 -0
- package/dist_ts/discovery/discovery.classes.ssdp.js +255 -0
- package/dist_ts/factories/index.d.ts +190 -0
- package/dist_ts/factories/index.js +350 -0
- package/dist_ts/features/feature.abstract.d.ts +118 -0
- package/dist_ts/features/feature.abstract.js +172 -0
- package/dist_ts/features/feature.camera.d.ts +95 -0
- package/dist_ts/features/feature.camera.js +162 -0
- package/dist_ts/features/feature.climate.d.ts +174 -0
- package/dist_ts/features/feature.climate.js +328 -0
- package/dist_ts/features/feature.cover.d.ts +121 -0
- package/dist_ts/features/feature.cover.js +217 -0
- package/dist_ts/features/feature.fan.d.ts +127 -0
- package/dist_ts/features/feature.fan.js +235 -0
- package/dist_ts/features/feature.light.d.ts +156 -0
- package/dist_ts/features/feature.light.js +296 -0
- package/dist_ts/features/feature.lock.d.ts +103 -0
- package/dist_ts/features/feature.lock.js +170 -0
- package/dist_ts/features/feature.playback.d.ts +98 -0
- package/dist_ts/features/feature.playback.js +191 -0
- package/dist_ts/features/feature.power.d.ts +79 -0
- package/dist_ts/features/feature.power.js +173 -0
- package/dist_ts/features/feature.print.d.ts +78 -0
- package/dist_ts/features/feature.print.js +226 -0
- package/dist_ts/features/feature.scan.d.ts +79 -0
- package/dist_ts/features/feature.scan.js +315 -0
- package/dist_ts/features/feature.sensor.d.ts +95 -0
- package/dist_ts/features/feature.sensor.js +147 -0
- package/dist_ts/features/feature.snmp.d.ts +88 -0
- package/dist_ts/features/feature.snmp.js +175 -0
- package/dist_ts/features/feature.switch.d.ts +75 -0
- package/dist_ts/features/feature.switch.js +126 -0
- package/dist_ts/features/feature.volume.d.ts +104 -0
- package/dist_ts/features/feature.volume.js +186 -0
- package/dist_ts/features/index.d.ts +19 -0
- package/dist_ts/features/index.js +23 -0
- package/dist_ts/helpers/helpers.iprange.d.ts +35 -0
- package/dist_ts/helpers/helpers.iprange.js +151 -0
- package/dist_ts/helpers/helpers.retry.d.ts +25 -0
- package/dist_ts/helpers/helpers.retry.js +69 -0
- package/dist_ts/index.d.ts +15 -0
- package/dist_ts/index.js +40 -0
- package/dist_ts/interfaces/feature.interfaces.d.ts +255 -0
- package/dist_ts/interfaces/feature.interfaces.js +6 -0
- package/dist_ts/interfaces/homeassistant.interfaces.d.ts +550 -0
- package/dist_ts/interfaces/homeassistant.interfaces.js +111 -0
- package/dist_ts/interfaces/index.d.ts +330 -0
- package/dist_ts/interfaces/index.js +16 -0
- package/dist_ts/interfaces/smarthome.interfaces.d.ts +282 -0
- package/dist_ts/interfaces/smarthome.interfaces.js +7 -0
- package/dist_ts/paths.d.ts +1 -0
- package/dist_ts/paths.js +3 -0
- package/dist_ts/plugins.d.ts +23 -0
- package/dist_ts/plugins.js +30 -0
- package/dist_ts/protocols/index.d.ts +12 -0
- package/dist_ts/protocols/index.js +21 -0
- package/dist_ts/protocols/protocol.escl.d.ts +58 -0
- package/dist_ts/protocols/protocol.escl.js +364 -0
- package/dist_ts/protocols/protocol.homeassistant.d.ts +248 -0
- package/dist_ts/protocols/protocol.homeassistant.js +611 -0
- package/dist_ts/protocols/protocol.ipp.d.ts +45 -0
- package/dist_ts/protocols/protocol.ipp.js +284 -0
- package/dist_ts/protocols/protocol.nut.d.ts +160 -0
- package/dist_ts/protocols/protocol.nut.js +364 -0
- package/dist_ts/protocols/protocol.sane.d.ts +104 -0
- package/dist_ts/protocols/protocol.sane.js +597 -0
- package/dist_ts/protocols/protocol.snmp.d.ts +133 -0
- package/dist_ts/protocols/protocol.snmp.js +333 -0
- package/dist_ts/protocols/protocol.upnp.d.ts +207 -0
- package/dist_ts/protocols/protocol.upnp.js +442 -0
- package/dist_ts/protocols/protocol.upssnmp.d.ts +160 -0
- package/dist_ts/protocols/protocol.upssnmp.js +263 -0
- package/npmextra.json +24 -0
- package/package.json +39 -0
- package/readme.hints.md +115 -0
- package/readme.md +624 -0
- package/test/test.ts +261 -0
- package/ts/00_commitinfo_data.ts +8 -0
- package/ts/device/device.classes.device.ts +454 -0
- package/ts/devicemanager.classes.devicemanager.ts +1219 -0
- package/ts/discovery/discovery.classes.homeassistant.ts +376 -0
- package/ts/discovery/discovery.classes.mdns.ts +347 -0
- package/ts/discovery/discovery.classes.networkscanner.ts +648 -0
- package/ts/discovery/discovery.classes.ssdp.ts +357 -0
- package/ts/factories/index.ts +696 -0
- package/ts/features/feature.abstract.ts +251 -0
- package/ts/features/feature.camera.ts +214 -0
- package/ts/features/feature.climate.ts +407 -0
- package/ts/features/feature.cover.ts +278 -0
- package/ts/features/feature.fan.ts +296 -0
- package/ts/features/feature.light.ts +369 -0
- package/ts/features/feature.lock.ts +223 -0
- package/ts/features/feature.playback.ts +246 -0
- package/ts/features/feature.power.ts +231 -0
- package/ts/features/feature.print.ts +297 -0
- package/ts/features/feature.scan.ts +370 -0
- package/ts/features/feature.sensor.ts +202 -0
- package/ts/features/feature.snmp.ts +235 -0
- package/ts/features/feature.switch.ts +170 -0
- package/ts/features/feature.volume.ts +256 -0
- package/ts/features/index.ts +25 -0
- package/ts/helpers/helpers.iprange.ts +172 -0
- package/ts/helpers/helpers.retry.ts +100 -0
- package/ts/index.ts +165 -0
- package/ts/interfaces/feature.interfaces.ts +359 -0
- package/ts/interfaces/homeassistant.interfaces.ts +666 -0
- package/ts/interfaces/index.ts +421 -0
- package/ts/interfaces/smarthome.interfaces.ts +421 -0
- package/ts/paths.ts +5 -0
- package/ts/plugins.ts +41 -0
- package/ts/protocols/index.ts +59 -0
- package/ts/protocols/protocol.escl.ts +439 -0
- package/ts/protocols/protocol.homeassistant.ts +737 -0
- package/ts/protocols/protocol.ipp.ts +329 -0
- package/ts/protocols/protocol.nut.ts +471 -0
- package/ts/protocols/protocol.sane.ts +694 -0
- package/ts/protocols/protocol.snmp.ts +439 -0
- package/ts/protocols/protocol.upnp.ts +627 -0
- package/ts/protocols/protocol.upssnmp.ts +377 -0
- package/tsconfig.json +12 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Volume Feature
|
|
3
|
+
* Provides volume control capability
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Feature, type TDeviceReference } from './feature.abstract.js';
|
|
7
|
+
import type {
|
|
8
|
+
IVolumeFeatureInfo,
|
|
9
|
+
IFeatureOptions,
|
|
10
|
+
} from '../interfaces/feature.interfaces.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Options for creating a VolumeFeature
|
|
14
|
+
*/
|
|
15
|
+
export interface IVolumeFeatureOptions extends IFeatureOptions {
|
|
16
|
+
/** Volume control protocol (usually same as device protocol) */
|
|
17
|
+
volumeProtocol?: string;
|
|
18
|
+
/** Minimum volume level */
|
|
19
|
+
minVolume?: number;
|
|
20
|
+
/** Maximum volume level */
|
|
21
|
+
maxVolume?: number;
|
|
22
|
+
/** Volume step increment */
|
|
23
|
+
volumeStep?: number;
|
|
24
|
+
/** Whether mute is supported */
|
|
25
|
+
supportsMute?: boolean;
|
|
26
|
+
/** Protocol-specific volume controller */
|
|
27
|
+
volumeController?: IVolumeController;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Interface for protocol-specific volume control
|
|
32
|
+
*/
|
|
33
|
+
export interface IVolumeController {
|
|
34
|
+
getVolume(): Promise<number>;
|
|
35
|
+
setVolume(level: number): Promise<void>;
|
|
36
|
+
getMute(): Promise<boolean>;
|
|
37
|
+
setMute(muted: boolean): Promise<void>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Volume Feature - provides volume control capability
|
|
42
|
+
*
|
|
43
|
+
* Separated from PlaybackFeature because some devices have volume control
|
|
44
|
+
* without playback capability (e.g., amplifiers, HDMI matrix switches).
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```typescript
|
|
48
|
+
* const volume = device.getFeature<VolumeFeature>('volume');
|
|
49
|
+
* if (volume) {
|
|
50
|
+
* const current = await volume.getVolume();
|
|
51
|
+
* await volume.setVolume(current + 10);
|
|
52
|
+
* await volume.toggleMute();
|
|
53
|
+
* }
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
export class VolumeFeature extends Feature {
|
|
57
|
+
public readonly type = 'volume' as const;
|
|
58
|
+
public readonly protocol: string;
|
|
59
|
+
|
|
60
|
+
// Capabilities
|
|
61
|
+
public readonly minVolume: number;
|
|
62
|
+
public readonly maxVolume: number;
|
|
63
|
+
public readonly volumeStep: number;
|
|
64
|
+
public readonly supportsMute: boolean;
|
|
65
|
+
|
|
66
|
+
// Current state
|
|
67
|
+
protected _volume: number = 0;
|
|
68
|
+
protected _muted: boolean = false;
|
|
69
|
+
|
|
70
|
+
// Volume controller (protocol-specific)
|
|
71
|
+
protected volumeController: IVolumeController | null = null;
|
|
72
|
+
|
|
73
|
+
constructor(
|
|
74
|
+
device: TDeviceReference,
|
|
75
|
+
port: number,
|
|
76
|
+
options?: IVolumeFeatureOptions
|
|
77
|
+
) {
|
|
78
|
+
super(device, port, options);
|
|
79
|
+
this.protocol = options?.volumeProtocol ?? 'generic';
|
|
80
|
+
this.minVolume = options?.minVolume ?? 0;
|
|
81
|
+
this.maxVolume = options?.maxVolume ?? 100;
|
|
82
|
+
this.volumeStep = options?.volumeStep ?? 5;
|
|
83
|
+
this.supportsMute = options?.supportsMute ?? true;
|
|
84
|
+
|
|
85
|
+
if (options?.volumeController) {
|
|
86
|
+
this.volumeController = options.volumeController;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ============================================================================
|
|
91
|
+
// Properties
|
|
92
|
+
// ============================================================================
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Get current volume level (cached)
|
|
96
|
+
*/
|
|
97
|
+
public get volume(): number {
|
|
98
|
+
return this._volume;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Get mute state (cached)
|
|
103
|
+
*/
|
|
104
|
+
public get muted(): boolean {
|
|
105
|
+
return this._muted;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ============================================================================
|
|
109
|
+
// Connection
|
|
110
|
+
// ============================================================================
|
|
111
|
+
|
|
112
|
+
protected async doConnect(): Promise<void> {
|
|
113
|
+
// Fetch initial state if we have a controller
|
|
114
|
+
if (this.volumeController) {
|
|
115
|
+
try {
|
|
116
|
+
this._volume = await this.volumeController.getVolume();
|
|
117
|
+
this._muted = await this.volumeController.getMute();
|
|
118
|
+
} catch {
|
|
119
|
+
// Ignore errors fetching initial state
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
protected async doDisconnect(): Promise<void> {
|
|
125
|
+
// Nothing to disconnect for volume control
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ============================================================================
|
|
129
|
+
// Volume Control
|
|
130
|
+
// ============================================================================
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Get current volume level
|
|
134
|
+
*/
|
|
135
|
+
public async getVolume(): Promise<number> {
|
|
136
|
+
if (this.volumeController) {
|
|
137
|
+
this._volume = await this.volumeController.getVolume();
|
|
138
|
+
}
|
|
139
|
+
return this._volume;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Set volume level
|
|
144
|
+
* @param level Volume level (clamped to min/max)
|
|
145
|
+
*/
|
|
146
|
+
public async setVolume(level: number): Promise<void> {
|
|
147
|
+
const clampedLevel = Math.max(this.minVolume, Math.min(this.maxVolume, level));
|
|
148
|
+
|
|
149
|
+
if (this.volumeController) {
|
|
150
|
+
await this.volumeController.setVolume(clampedLevel);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const oldVolume = this._volume;
|
|
154
|
+
this._volume = clampedLevel;
|
|
155
|
+
|
|
156
|
+
if (oldVolume !== clampedLevel) {
|
|
157
|
+
this.emit('volume:changed', { oldVolume, newVolume: clampedLevel });
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Increase volume by step
|
|
163
|
+
*/
|
|
164
|
+
public async volumeUp(step?: number): Promise<number> {
|
|
165
|
+
const increment = step ?? this.volumeStep;
|
|
166
|
+
const newVolume = Math.min(this.maxVolume, this._volume + increment);
|
|
167
|
+
await this.setVolume(newVolume);
|
|
168
|
+
return this._volume;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Decrease volume by step
|
|
173
|
+
*/
|
|
174
|
+
public async volumeDown(step?: number): Promise<number> {
|
|
175
|
+
const decrement = step ?? this.volumeStep;
|
|
176
|
+
const newVolume = Math.max(this.minVolume, this._volume - decrement);
|
|
177
|
+
await this.setVolume(newVolume);
|
|
178
|
+
return this._volume;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Get mute state
|
|
183
|
+
*/
|
|
184
|
+
public async getMute(): Promise<boolean> {
|
|
185
|
+
if (this.volumeController && this.supportsMute) {
|
|
186
|
+
this._muted = await this.volumeController.getMute();
|
|
187
|
+
}
|
|
188
|
+
return this._muted;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Set mute state
|
|
193
|
+
*/
|
|
194
|
+
public async setMute(muted: boolean): Promise<void> {
|
|
195
|
+
if (!this.supportsMute) {
|
|
196
|
+
throw new Error('Mute not supported');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (this.volumeController) {
|
|
200
|
+
await this.volumeController.setMute(muted);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const oldMuted = this._muted;
|
|
204
|
+
this._muted = muted;
|
|
205
|
+
|
|
206
|
+
if (oldMuted !== muted) {
|
|
207
|
+
this.emit('mute:changed', { oldMuted, newMuted: muted });
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Toggle mute state
|
|
213
|
+
*/
|
|
214
|
+
public async toggleMute(): Promise<boolean> {
|
|
215
|
+
const currentMuted = await this.getMute();
|
|
216
|
+
await this.setMute(!currentMuted);
|
|
217
|
+
return this._muted;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ============================================================================
|
|
221
|
+
// Serialization
|
|
222
|
+
// ============================================================================
|
|
223
|
+
|
|
224
|
+
public getFeatureInfo(): IVolumeFeatureInfo {
|
|
225
|
+
return {
|
|
226
|
+
...this.getBaseFeatureInfo(),
|
|
227
|
+
type: 'volume',
|
|
228
|
+
minVolume: this.minVolume,
|
|
229
|
+
maxVolume: this.maxVolume,
|
|
230
|
+
volumeStep: this.volumeStep,
|
|
231
|
+
supportsMute: this.supportsMute,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ============================================================================
|
|
236
|
+
// Static Factory
|
|
237
|
+
// ============================================================================
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Create from discovery metadata
|
|
241
|
+
*/
|
|
242
|
+
public static fromDiscovery(
|
|
243
|
+
device: TDeviceReference,
|
|
244
|
+
port: number,
|
|
245
|
+
protocol: string,
|
|
246
|
+
metadata: Record<string, unknown>
|
|
247
|
+
): VolumeFeature {
|
|
248
|
+
return new VolumeFeature(device, port, {
|
|
249
|
+
volumeProtocol: protocol,
|
|
250
|
+
minVolume: metadata.minVolume as number ?? 0,
|
|
251
|
+
maxVolume: metadata.maxVolume as number ?? 100,
|
|
252
|
+
volumeStep: metadata.volumeStep as number ?? 5,
|
|
253
|
+
supportsMute: metadata.supportsMute as boolean ?? true,
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Features Module
|
|
3
|
+
* Exports all feature classes and types
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Abstract base
|
|
7
|
+
export { Feature, type TDeviceReference } from './feature.abstract.js';
|
|
8
|
+
|
|
9
|
+
// Concrete features - Document/Infrastructure
|
|
10
|
+
export { ScanFeature, type IScanFeatureOptions } from './feature.scan.js';
|
|
11
|
+
export { PrintFeature, type IPrintFeatureOptions } from './feature.print.js';
|
|
12
|
+
export { PlaybackFeature, type IPlaybackFeatureOptions } from './feature.playback.js';
|
|
13
|
+
export { VolumeFeature, type IVolumeFeatureOptions, type IVolumeController } from './feature.volume.js';
|
|
14
|
+
export { PowerFeature, type IPowerFeatureOptions } from './feature.power.js';
|
|
15
|
+
export { SnmpFeature, type ISnmpFeatureOptions } from './feature.snmp.js';
|
|
16
|
+
|
|
17
|
+
// Smart Home Features (protocol-agnostic: home-assistant, hue, mqtt, etc.)
|
|
18
|
+
export { SwitchFeature, type ISwitchFeatureOptions } from './feature.switch.js';
|
|
19
|
+
export { SensorFeature, type ISensorFeatureOptions } from './feature.sensor.js';
|
|
20
|
+
export { LightFeature, type ILightFeatureOptions } from './feature.light.js';
|
|
21
|
+
export { CoverFeature, type ICoverFeatureOptions } from './feature.cover.js';
|
|
22
|
+
export { LockFeature, type ILockFeatureOptions } from './feature.lock.js';
|
|
23
|
+
export { FanFeature, type IFanFeatureOptions } from './feature.fan.js';
|
|
24
|
+
export { ClimateFeature, type IClimateFeatureOptions } from './feature.climate.js';
|
|
25
|
+
export { CameraFeature, type ICameraFeatureOptions } from './feature.camera.js';
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IP Range utility functions for network scanning
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as os from 'os';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Validates an IPv4 address
|
|
9
|
+
*/
|
|
10
|
+
export function isValidIp(ip: string): boolean {
|
|
11
|
+
const parts = ip.split('.');
|
|
12
|
+
if (parts.length !== 4) return false;
|
|
13
|
+
|
|
14
|
+
return parts.every((part) => {
|
|
15
|
+
const num = parseInt(part, 10);
|
|
16
|
+
return !isNaN(num) && num >= 0 && num <= 255 && part === num.toString();
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Converts an IPv4 address to a 32-bit number
|
|
22
|
+
*/
|
|
23
|
+
export function ipToNumber(ip: string): number {
|
|
24
|
+
const parts = ip.split('.').map((p) => parseInt(p, 10));
|
|
25
|
+
return ((parts[0] << 24) | (parts[1] << 16) | (parts[2] << 8) | parts[3]) >>> 0;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Converts a 32-bit number to an IPv4 address
|
|
30
|
+
*/
|
|
31
|
+
export function numberToIp(num: number): string {
|
|
32
|
+
return [
|
|
33
|
+
(num >>> 24) & 255,
|
|
34
|
+
(num >>> 16) & 255,
|
|
35
|
+
(num >>> 8) & 255,
|
|
36
|
+
num & 255,
|
|
37
|
+
].join('.');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Generates an array of IP addresses from a start to end range
|
|
42
|
+
* Excludes network address (.0) and broadcast address (.255) for /24 ranges
|
|
43
|
+
*/
|
|
44
|
+
export function ipRangeToIps(startIp: string, endIp: string): string[] {
|
|
45
|
+
if (!isValidIp(startIp) || !isValidIp(endIp)) {
|
|
46
|
+
throw new Error(`Invalid IP address: ${!isValidIp(startIp) ? startIp : endIp}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const start = ipToNumber(startIp);
|
|
50
|
+
const end = ipToNumber(endIp);
|
|
51
|
+
|
|
52
|
+
if (start > end) {
|
|
53
|
+
throw new Error(`Start IP (${startIp}) must be less than or equal to end IP (${endIp})`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const ips: string[] = [];
|
|
57
|
+
for (let i = start; i <= end; i++) {
|
|
58
|
+
ips.push(numberToIp(i));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return ips;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Parses CIDR notation and returns an array of usable host IPs
|
|
66
|
+
* Excludes network address and broadcast address
|
|
67
|
+
* Example: "192.168.1.0/24" returns 192.168.1.1 through 192.168.1.254
|
|
68
|
+
*/
|
|
69
|
+
export function cidrToIps(cidr: string): string[] {
|
|
70
|
+
const match = cidr.match(/^(\d+\.\d+\.\d+\.\d+)\/(\d+)$/);
|
|
71
|
+
if (!match) {
|
|
72
|
+
throw new Error(`Invalid CIDR notation: ${cidr}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const [, networkIp, prefixStr] = match;
|
|
76
|
+
const prefix = parseInt(prefixStr, 10);
|
|
77
|
+
|
|
78
|
+
if (!isValidIp(networkIp)) {
|
|
79
|
+
throw new Error(`Invalid network address: ${networkIp}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (prefix < 0 || prefix > 32) {
|
|
83
|
+
throw new Error(`Invalid prefix length: ${prefix}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// For /32, just return the single IP
|
|
87
|
+
if (prefix === 32) {
|
|
88
|
+
return [networkIp];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// For /31 (point-to-point), return both IPs
|
|
92
|
+
if (prefix === 31) {
|
|
93
|
+
const networkNum = ipToNumber(networkIp);
|
|
94
|
+
const mask = (0xffffffff << (32 - prefix)) >>> 0;
|
|
95
|
+
const network = (networkNum & mask) >>> 0;
|
|
96
|
+
return [numberToIp(network), numberToIp(network + 1)];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Calculate network and broadcast addresses
|
|
100
|
+
const networkNum = ipToNumber(networkIp);
|
|
101
|
+
const mask = (0xffffffff << (32 - prefix)) >>> 0;
|
|
102
|
+
const network = (networkNum & mask) >>> 0;
|
|
103
|
+
const broadcast = (network | (~mask >>> 0)) >>> 0;
|
|
104
|
+
|
|
105
|
+
// Generate usable host IPs (exclude network and broadcast)
|
|
106
|
+
const ips: string[] = [];
|
|
107
|
+
for (let i = network + 1; i < broadcast; i++) {
|
|
108
|
+
ips.push(numberToIp(i));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return ips;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Gets the local network interfaces and returns the first non-loopback IPv4 subnet
|
|
116
|
+
* Returns CIDR notation (e.g., "192.168.1.0/24")
|
|
117
|
+
*/
|
|
118
|
+
export function getLocalSubnet(): string | null {
|
|
119
|
+
try {
|
|
120
|
+
const interfaces = os.networkInterfaces();
|
|
121
|
+
|
|
122
|
+
for (const name of Object.keys(interfaces)) {
|
|
123
|
+
const iface = interfaces[name];
|
|
124
|
+
if (!iface) continue;
|
|
125
|
+
|
|
126
|
+
for (const info of iface) {
|
|
127
|
+
// Skip loopback and non-IPv4
|
|
128
|
+
if (info.family !== 'IPv4' || info.internal) continue;
|
|
129
|
+
|
|
130
|
+
// Calculate the network address from IP and netmask
|
|
131
|
+
const ip = ipToNumber(info.address);
|
|
132
|
+
const mask = ipToNumber(info.netmask);
|
|
133
|
+
const network = (ip & mask) >>> 0;
|
|
134
|
+
|
|
135
|
+
// Calculate prefix length from netmask
|
|
136
|
+
const maskBits = info.netmask.split('.').reduce((acc: number, octet: string) => {
|
|
137
|
+
const byte = parseInt(octet, 10);
|
|
138
|
+
let bits = 0;
|
|
139
|
+
for (let i = 7; i >= 0; i--) {
|
|
140
|
+
if ((byte >> i) & 1) bits++;
|
|
141
|
+
else break;
|
|
142
|
+
}
|
|
143
|
+
return acc + bits;
|
|
144
|
+
}, 0);
|
|
145
|
+
|
|
146
|
+
return `${numberToIp(network)}/${maskBits}`;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
} catch {
|
|
150
|
+
// Failed to get network interfaces
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Counts the number of IPs in a CIDR range (excluding network and broadcast)
|
|
158
|
+
*/
|
|
159
|
+
export function countIpsInCidr(cidr: string): number {
|
|
160
|
+
const match = cidr.match(/^(\d+\.\d+\.\d+\.\d+)\/(\d+)$/);
|
|
161
|
+
if (!match) {
|
|
162
|
+
throw new Error(`Invalid CIDR notation: ${cidr}`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const prefix = parseInt(match[2], 10);
|
|
166
|
+
|
|
167
|
+
if (prefix === 32) return 1;
|
|
168
|
+
if (prefix === 31) return 2;
|
|
169
|
+
|
|
170
|
+
// 2^(32-prefix) - 2 (minus network and broadcast)
|
|
171
|
+
return Math.pow(2, 32 - prefix) - 2;
|
|
172
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import * as plugins from '../plugins.js';
|
|
2
|
+
import type { IRetryOptions } from '../interfaces/index.js';
|
|
3
|
+
|
|
4
|
+
const defaultRetryOptions: Required<IRetryOptions> = {
|
|
5
|
+
maxRetries: 5,
|
|
6
|
+
baseDelay: 1000,
|
|
7
|
+
maxDelay: 16000,
|
|
8
|
+
multiplier: 2,
|
|
9
|
+
jitter: true,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Calculates the delay for a retry attempt using exponential backoff
|
|
14
|
+
*/
|
|
15
|
+
function calculateDelay(
|
|
16
|
+
attempt: number,
|
|
17
|
+
options: Required<IRetryOptions>
|
|
18
|
+
): number {
|
|
19
|
+
const delay = Math.min(
|
|
20
|
+
options.baseDelay * Math.pow(options.multiplier, attempt),
|
|
21
|
+
options.maxDelay
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
if (options.jitter) {
|
|
25
|
+
// Add random jitter of +/- 25%
|
|
26
|
+
const jitterRange = delay * 0.25;
|
|
27
|
+
return delay + (Math.random() * jitterRange * 2 - jitterRange);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return delay;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Executes a function with retry logic using exponential backoff
|
|
35
|
+
*
|
|
36
|
+
* @param fn - The async function to execute
|
|
37
|
+
* @param options - Retry configuration options
|
|
38
|
+
* @returns The result of the function
|
|
39
|
+
* @throws The last error if all retries fail
|
|
40
|
+
*/
|
|
41
|
+
export async function withRetry<T>(
|
|
42
|
+
fn: () => Promise<T>,
|
|
43
|
+
options?: IRetryOptions
|
|
44
|
+
): Promise<T> {
|
|
45
|
+
const opts: Required<IRetryOptions> = { ...defaultRetryOptions, ...options };
|
|
46
|
+
let lastError: Error | undefined;
|
|
47
|
+
|
|
48
|
+
for (let attempt = 0; attempt <= opts.maxRetries; attempt++) {
|
|
49
|
+
try {
|
|
50
|
+
return await fn();
|
|
51
|
+
} catch (error) {
|
|
52
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
53
|
+
|
|
54
|
+
if (attempt === opts.maxRetries) {
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const delay = calculateDelay(attempt, opts);
|
|
59
|
+
await plugins.smartdelay.delayFor(delay);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
throw lastError ?? new Error('Retry failed with unknown error');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Creates a retryable version of an async function
|
|
68
|
+
*
|
|
69
|
+
* @param fn - The async function to wrap
|
|
70
|
+
* @param options - Retry configuration options
|
|
71
|
+
* @returns A wrapped function that retries on failure
|
|
72
|
+
*/
|
|
73
|
+
export function createRetryable<TArgs extends unknown[], TResult>(
|
|
74
|
+
fn: (...args: TArgs) => Promise<TResult>,
|
|
75
|
+
options?: IRetryOptions
|
|
76
|
+
): (...args: TArgs) => Promise<TResult> {
|
|
77
|
+
return (...args: TArgs) => withRetry(() => fn(...args), options);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Retry decorator for class methods
|
|
82
|
+
* Note: Use as a wrapper function since TC39 decorators have different semantics
|
|
83
|
+
*/
|
|
84
|
+
export function retryMethod<T extends Record<string, unknown>>(
|
|
85
|
+
target: T,
|
|
86
|
+
methodName: keyof T & string,
|
|
87
|
+
options?: IRetryOptions
|
|
88
|
+
): void {
|
|
89
|
+
const originalMethod = target[methodName];
|
|
90
|
+
if (typeof originalMethod !== 'function') {
|
|
91
|
+
throw new Error(`${methodName} is not a function`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
target[methodName] = createRetryable(
|
|
95
|
+
originalMethod.bind(target) as (...args: unknown[]) => Promise<unknown>,
|
|
96
|
+
options
|
|
97
|
+
) as T[typeof methodName];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export { defaultRetryOptions };
|