homebridge-generac 0.1.0-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.
Files changed (71) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/LICENSE +200 -0
  3. package/NOTICE +20 -0
  4. package/README.md +179 -0
  5. package/SECURITY.md +17 -0
  6. package/config.schema.json +133 -0
  7. package/dist/accessory.d.ts +54 -0
  8. package/dist/accessory.js +191 -0
  9. package/dist/accessory.js.map +1 -0
  10. package/dist/api.d.ts +43 -0
  11. package/dist/api.js +124 -0
  12. package/dist/api.js.map +1 -0
  13. package/dist/attention.d.ts +18 -0
  14. package/dist/attention.js +40 -0
  15. package/dist/attention.js.map +1 -0
  16. package/dist/auth.d.ts +56 -0
  17. package/dist/auth.js +412 -0
  18. package/dist/auth.js.map +1 -0
  19. package/dist/captures.d.ts +9 -0
  20. package/dist/captures.js +42 -0
  21. package/dist/captures.js.map +1 -0
  22. package/dist/cli.d.ts +2 -0
  23. package/dist/cli.js +116 -0
  24. package/dist/cli.js.map +1 -0
  25. package/dist/exercise.d.ts +42 -0
  26. package/dist/exercise.js +115 -0
  27. package/dist/exercise.js.map +1 -0
  28. package/dist/files.d.ts +5 -0
  29. package/dist/files.js +28 -0
  30. package/dist/files.js.map +1 -0
  31. package/dist/index.d.ts +3 -0
  32. package/dist/index.js +6 -0
  33. package/dist/index.js.map +1 -0
  34. package/dist/model.d.ts +74 -0
  35. package/dist/model.js +167 -0
  36. package/dist/model.js.map +1 -0
  37. package/dist/platform.d.ts +117 -0
  38. package/dist/platform.js +596 -0
  39. package/dist/platform.js.map +1 -0
  40. package/dist/settings.d.ts +75 -0
  41. package/dist/settings.js +115 -0
  42. package/dist/settings.js.map +1 -0
  43. package/dist/state.d.ts +28 -0
  44. package/dist/state.js +43 -0
  45. package/dist/state.js.map +1 -0
  46. package/dist/types.d.ts +98 -0
  47. package/dist/types.js +48 -0
  48. package/dist/types.js.map +1 -0
  49. package/dist/ui/server.d.ts +96 -0
  50. package/dist/ui/server.js +328 -0
  51. package/dist/ui/server.js.map +1 -0
  52. package/homebridge-ui/public/generac-banner.png +0 -0
  53. package/homebridge-ui/public/generac.css +255 -0
  54. package/homebridge-ui/public/index.css +510 -0
  55. package/homebridge-ui/public/index.html +14 -0
  56. package/homebridge-ui/public/js/api.js +31 -0
  57. package/homebridge-ui/public/js/app.js +1 -0
  58. package/homebridge-ui/public/js/card.js +27 -0
  59. package/homebridge-ui/public/js/copy.js +167 -0
  60. package/homebridge-ui/public/js/dom.js +247 -0
  61. package/homebridge-ui/public/js/footer.js +26 -0
  62. package/homebridge-ui/public/js/format.js +62 -0
  63. package/homebridge-ui/public/js/main.js +288 -0
  64. package/homebridge-ui/public/js/mark.js +34 -0
  65. package/homebridge-ui/public/js/model.js +120 -0
  66. package/homebridge-ui/public/js/sections/account.js +272 -0
  67. package/homebridge-ui/public/js/sections/generators.js +165 -0
  68. package/homebridge-ui/public/js/sections/settings.js +114 -0
  69. package/homebridge-ui/public/js/validate.js +45 -0
  70. package/homebridge-ui/server.js +8 -0
  71. package/package.json +70 -0
@@ -0,0 +1,75 @@
1
+ export declare const PLATFORM_NAME = "Generac";
2
+ export declare const PLUGIN_NAME = "homebridge-generac";
3
+ /** Display-name override for one generator, keyed by Mobile Link apparatusId. */
4
+ export interface GeneratorOverride {
5
+ apparatusId: number;
6
+ name: string;
7
+ }
8
+ /** Keys accepted in config.json. See SPEC section 9. */
9
+ export interface GeneracConfig {
10
+ name?: string;
11
+ credentialsPath?: string;
12
+ pollIdleMinutes?: number;
13
+ pollActiveSeconds?: number;
14
+ batteryLowVoltage?: number;
15
+ faultOnStopped?: boolean;
16
+ faultOnDisconnected?: boolean;
17
+ attentionSensor?: boolean;
18
+ exerciseSensor?: boolean;
19
+ exerciseTime?: string;
20
+ exerciseHoldMinutes?: number;
21
+ debug?: boolean;
22
+ generators?: GeneratorOverride[];
23
+ }
24
+ export interface ResolvedConfig {
25
+ pollIdleMinutes: number;
26
+ pollActiveSeconds: number;
27
+ batteryLowVoltage: number;
28
+ faultOnStopped: boolean;
29
+ faultOnDisconnected: boolean;
30
+ attentionSensor: boolean;
31
+ exerciseSensor: boolean;
32
+ /** 24-hour "HH:MM", or undefined to use the API's Exercise Minutes (SPEC section 9). Malformed values read as undefined. */
33
+ exerciseTime: string | undefined;
34
+ exerciseHoldMinutes: number;
35
+ debug: boolean;
36
+ generators: GeneratorOverride[];
37
+ }
38
+ export declare const POLL_IDLE_MINUTES_DEFAULT = 10;
39
+ export declare const POLL_IDLE_MINUTES_MIN = 2;
40
+ export declare const POLL_ACTIVE_SECONDS_DEFAULT = 90;
41
+ export declare const POLL_ACTIVE_SECONDS_MIN = 60;
42
+ export declare const EXERCISE_HOLD_MINUTES_DEFAULT = 5;
43
+ export declare const EXERCISE_HOLD_MINUTES_MIN = 1;
44
+ /** A 24-hour "HH:MM" time as the settings page and config.json carry it. */
45
+ export declare const HHMM_PATTERN: RegExp;
46
+ /** The `exerciseTime` setting: "HH:MM" trimmed, or undefined when absent, blank or malformed. */
47
+ export declare function exerciseTimeSetting(value: unknown): string | undefined;
48
+ export declare function resolveConfig(c: GeneracConfig): ResolvedConfig;
49
+ /** The display name for a generator: the configured override when present, else the Mobile Link name. */
50
+ export declare function displayNameFor(config: ResolvedConfig, apparatusId: number, mobileLinkName: string): string;
51
+ /** The plugin's data directory under the Homebridge storage path. */
52
+ export declare function dataDir(storagePath: string): string;
53
+ /** Where the platform and the UI server write credentials (SPEC section 4.4). */
54
+ export declare function primaryCredentialsPath(storagePath: string): string;
55
+ /** The marker the settings page's Reset leaves under the data directory: the platform removes every cached accessory on its next start. */
56
+ export declare const RESET_MARKER = "reset-pending";
57
+ /** Where the platform writes its state after every poll (SPEC section 10). */
58
+ export declare function statePath(storagePath: string): string;
59
+ /**
60
+ * Where the platform looks for credentials, in priority order (SPEC section 4.4):
61
+ * the primary location under the Homebridge storage path, then `credentialsPath`
62
+ * from config when set, then the conventional ~/.homebridge location.
63
+ */
64
+ export declare function credentialCandidates(storagePath: string | undefined, explicit?: string): string[];
65
+ export declare function defaultStoragePath(): string;
66
+ /**
67
+ * The numeric part of a version for HAP's FirmwareRevision (SPEC section 7): "0.1.0" for "0.1.0-beta.1".
68
+ * HAP truncates a pre-release suffix to "0.1", which is a different version.
69
+ */
70
+ export declare function firmwareVersion(version: string): string;
71
+ /**
72
+ * The plugin's own version from package.json, found by walking up from this
73
+ * file. Works from dist/ (published layout) and from the test build.
74
+ */
75
+ export declare function pluginVersion(): string;
@@ -0,0 +1,115 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ export const PLATFORM_NAME = 'Generac';
6
+ export const PLUGIN_NAME = 'homebridge-generac';
7
+ export const POLL_IDLE_MINUTES_DEFAULT = 10;
8
+ export const POLL_IDLE_MINUTES_MIN = 2;
9
+ export const POLL_ACTIVE_SECONDS_DEFAULT = 90;
10
+ export const POLL_ACTIVE_SECONDS_MIN = 60;
11
+ export const EXERCISE_HOLD_MINUTES_DEFAULT = 5;
12
+ export const EXERCISE_HOLD_MINUTES_MIN = 1;
13
+ /** A 24-hour "HH:MM" time as the settings page and config.json carry it. */
14
+ export const HHMM_PATTERN = /^([01]\d|2[0-3]):[0-5]\d$/;
15
+ /** The `exerciseTime` setting: "HH:MM" trimmed, or undefined when absent, blank or malformed. */
16
+ export function exerciseTimeSetting(value) {
17
+ if (typeof value !== 'string') {
18
+ return undefined;
19
+ }
20
+ const v = value.trim();
21
+ return HHMM_PATTERN.test(v) ? v : undefined;
22
+ }
23
+ function finiteOr(value, fallback) {
24
+ const n = typeof value === 'number' ? value : Number(value);
25
+ return Number.isFinite(n) ? n : fallback;
26
+ }
27
+ export function resolveConfig(c) {
28
+ const overrides = Array.isArray(c.generators) ? c.generators : [];
29
+ return {
30
+ pollIdleMinutes: Math.max(POLL_IDLE_MINUTES_MIN, finiteOr(c.pollIdleMinutes, POLL_IDLE_MINUTES_DEFAULT)),
31
+ pollActiveSeconds: Math.max(POLL_ACTIVE_SECONDS_MIN, finiteOr(c.pollActiveSeconds, POLL_ACTIVE_SECONDS_DEFAULT)),
32
+ batteryLowVoltage: finiteOr(c.batteryLowVoltage, 12.0),
33
+ faultOnStopped: c.faultOnStopped ?? true,
34
+ faultOnDisconnected: c.faultOnDisconnected ?? false,
35
+ attentionSensor: c.attentionSensor ?? false,
36
+ exerciseSensor: c.exerciseSensor ?? true,
37
+ exerciseTime: exerciseTimeSetting(c.exerciseTime),
38
+ exerciseHoldMinutes: Math.max(EXERCISE_HOLD_MINUTES_MIN, finiteOr(c.exerciseHoldMinutes, EXERCISE_HOLD_MINUTES_DEFAULT)),
39
+ debug: c.debug ?? false,
40
+ generators: overrides
41
+ .filter((g) => g && Number.isFinite(Number(g.apparatusId)) && typeof g.name === 'string' && g.name.trim() !== '')
42
+ .map((g) => ({ apparatusId: Number(g.apparatusId), name: g.name.trim() })),
43
+ };
44
+ }
45
+ /** The display name for a generator: the configured override when present, else the Mobile Link name. */
46
+ export function displayNameFor(config, apparatusId, mobileLinkName) {
47
+ const override = config.generators.find((g) => g.apparatusId === apparatusId);
48
+ return override ? override.name : mobileLinkName;
49
+ }
50
+ /** The plugin's data directory under the Homebridge storage path. */
51
+ export function dataDir(storagePath) {
52
+ return path.join(storagePath, PLUGIN_NAME);
53
+ }
54
+ /** Where the platform and the UI server write credentials (SPEC section 4.4). */
55
+ export function primaryCredentialsPath(storagePath) {
56
+ return path.join(dataDir(storagePath), 'credentials.json');
57
+ }
58
+ /** The marker the settings page's Reset leaves under the data directory: the platform removes every cached accessory on its next start. */
59
+ export const RESET_MARKER = 'reset-pending';
60
+ /** Where the platform writes its state after every poll (SPEC section 10). */
61
+ export function statePath(storagePath) {
62
+ return path.join(dataDir(storagePath), 'state.json');
63
+ }
64
+ /**
65
+ * Where the platform looks for credentials, in priority order (SPEC section 4.4):
66
+ * the primary location under the Homebridge storage path, then `credentialsPath`
67
+ * from config when set, then the conventional ~/.homebridge location.
68
+ */
69
+ export function credentialCandidates(storagePath, explicit) {
70
+ const out = [];
71
+ if (storagePath) {
72
+ out.push(primaryCredentialsPath(storagePath));
73
+ }
74
+ if (explicit && explicit.trim()) {
75
+ out.push(path.resolve(explicit.trim()));
76
+ }
77
+ out.push(path.join(os.homedir(), '.homebridge', PLUGIN_NAME, 'credentials.json'));
78
+ return [...new Set(out)];
79
+ }
80
+ export function defaultStoragePath() {
81
+ return process.env.UIX_STORAGE_PATH || process.env.HOMEBRIDGE_STORAGE_PATH || path.join(os.homedir(), '.homebridge');
82
+ }
83
+ /**
84
+ * The numeric part of a version for HAP's FirmwareRevision (SPEC section 7): "0.1.0" for "0.1.0-beta.1".
85
+ * HAP truncates a pre-release suffix to "0.1", which is a different version.
86
+ */
87
+ export function firmwareVersion(version) {
88
+ const m = /^\d+\.\d+\.\d+/.exec(version.trim());
89
+ return m ? m[0] : version;
90
+ }
91
+ /**
92
+ * The plugin's own version from package.json, found by walking up from this
93
+ * file. Works from dist/ (published layout) and from the test build.
94
+ */
95
+ export function pluginVersion() {
96
+ let dir = path.dirname(fileURLToPath(import.meta.url));
97
+ for (let i = 0; i < 5; i++) {
98
+ try {
99
+ const pkg = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'));
100
+ if (pkg.name === PLUGIN_NAME && typeof pkg.version === 'string') {
101
+ return pkg.version;
102
+ }
103
+ }
104
+ catch {
105
+ // keep walking
106
+ }
107
+ const parent = path.dirname(dir);
108
+ if (parent === dir) {
109
+ break;
110
+ }
111
+ dir = parent;
112
+ }
113
+ return '0.0.0';
114
+ }
115
+ //# sourceMappingURL=settings.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"settings.js","sourceRoot":"","sources":["../src/settings.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,CAAC,MAAM,aAAa,GAAG,SAAS,CAAC;AACvC,MAAM,CAAC,MAAM,WAAW,GAAG,oBAAoB,CAAC;AAwChD,MAAM,CAAC,MAAM,yBAAyB,GAAG,EAAE,CAAC;AAC5C,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,CAAC;AACvC,MAAM,CAAC,MAAM,2BAA2B,GAAG,EAAE,CAAC;AAC9C,MAAM,CAAC,MAAM,uBAAuB,GAAG,EAAE,CAAC;AAC1C,MAAM,CAAC,MAAM,6BAA6B,GAAG,CAAC,CAAC;AAC/C,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAAC,CAAC;AAE3C,4EAA4E;AAC5E,MAAM,CAAC,MAAM,YAAY,GAAG,2BAA2B,CAAC;AAExD,iGAAiG;AACjG,MAAM,UAAU,mBAAmB,CAAC,KAAc;IAChD,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,MAAM,CAAC,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IACvB,OAAO,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;AAC9C,CAAC;AAED,SAAS,QAAQ,CAAC,KAAc,EAAE,QAAgB;IAChD,MAAM,CAAC,GAAG,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAC5D,OAAO,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC;AAC3C,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,CAAgB;IAC5C,MAAM,SAAS,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,CAAC;IAClE,OAAO;QACL,eAAe,EAAE,IAAI,CAAC,GAAG,CAAC,qBAAqB,EAAE,QAAQ,CAAC,CAAC,CAAC,eAAe,EAAE,yBAAyB,CAAC,CAAC;QACxG,iBAAiB,EAAE,IAAI,CAAC,GAAG,CAAC,uBAAuB,EAAE,QAAQ,CAAC,CAAC,CAAC,iBAAiB,EAAE,2BAA2B,CAAC,CAAC;QAChH,iBAAiB,EAAE,QAAQ,CAAC,CAAC,CAAC,iBAAiB,EAAE,IAAI,CAAC;QACtD,cAAc,EAAE,CAAC,CAAC,cAAc,IAAI,IAAI;QACxC,mBAAmB,EAAE,CAAC,CAAC,mBAAmB,IAAI,KAAK;QACnD,eAAe,EAAE,CAAC,CAAC,eAAe,IAAI,KAAK;QAC3C,cAAc,EAAE,CAAC,CAAC,cAAc,IAAI,IAAI;QACxC,YAAY,EAAE,mBAAmB,CAAC,CAAC,CAAC,YAAY,CAAC;QACjD,mBAAmB,EAAE,IAAI,CAAC,GAAG,CAAC,yBAAyB,EAAE,QAAQ,CAAC,CAAC,CAAC,mBAAmB,EAAE,6BAA6B,CAAC,CAAC;QACxH,KAAK,EAAE,CAAC,CAAC,KAAK,IAAI,KAAK;QACvB,UAAU,EAAE,SAAS;aAClB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,IAAI,OAAO,CAAC,CAAC,IAAI,KAAK,QAAQ,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC;aAChH,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,WAAW,EAAE,MAAM,CAAC,CAAC,CAAC,WAAW,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;KAC7E,CAAC;AACJ,CAAC;AAED,yGAAyG;AACzG,MAAM,UAAU,cAAc,CAAC,MAAsB,EAAE,WAAmB,EAAE,cAAsB;IAChG,MAAM,QAAQ,GAAG,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,KAAK,WAAW,CAAC,CAAC;IAC9E,OAAO,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,cAAc,CAAC;AACnD,CAAC;AAED,qEAAqE;AACrE,MAAM,UAAU,OAAO,CAAC,WAAmB;IACzC,OAAO,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC;AAC7C,CAAC;AAED,iFAAiF;AACjF,MAAM,UAAU,sBAAsB,CAAC,WAAmB;IACxD,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,EAAE,kBAAkB,CAAC,CAAC;AAC7D,CAAC;AAED,2IAA2I;AAC3I,MAAM,CAAC,MAAM,YAAY,GAAG,eAAe,CAAC;AAE5C,8EAA8E;AAC9E,MAAM,UAAU,SAAS,CAAC,WAAmB;IAC3C,OAAO,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,EAAE,YAAY,CAAC,CAAC;AACvD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,oBAAoB,CAAC,WAA+B,EAAE,QAAiB;IACrF,MAAM,GAAG,GAAa,EAAE,CAAC;IACzB,IAAI,WAAW,EAAE,CAAC;QAChB,GAAG,CAAC,IAAI,CAAC,sBAAsB,CAAC,WAAW,CAAC,CAAC,CAAC;IAChD,CAAC;IACD,IAAI,QAAQ,IAAI,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC;QAChC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;IAC1C,CAAC;IACD,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,aAAa,EAAE,WAAW,EAAE,kBAAkB,CAAC,CAAC,CAAC;IAClF,OAAO,CAAC,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;AAC3B,CAAC;AAED,MAAM,UAAU,kBAAkB;IAChC,OAAO,OAAO,CAAC,GAAG,CAAC,gBAAgB,IAAI,OAAO,CAAC,GAAG,CAAC,uBAAuB,IAAI,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,aAAa,CAAC,CAAC;AACvH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,OAAe;IAC7C,MAAM,CAAC,GAAG,gBAAgB,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;IAChD,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;AAC5B,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,aAAa;IAC3B,IAAI,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IACvD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QAC3B,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,cAAc,CAAC,EAAE,MAAM,CAAC,CAAwC,CAAC;YACvH,IAAI,GAAG,CAAC,IAAI,KAAK,WAAW,IAAI,OAAO,GAAG,CAAC,OAAO,KAAK,QAAQ,EAAE,CAAC;gBAChE,OAAO,GAAG,CAAC,OAAO,CAAC;YACrB,CAAC;QACH,CAAC;QAAC,MAAM,CAAC;YACP,eAAe;QACjB,CAAC;QACD,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QACjC,IAAI,MAAM,KAAK,GAAG,EAAE,CAAC;YACnB,MAAM;QACR,CAAC;QACD,GAAG,GAAG,MAAM,CAAC;IACf,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC"}
@@ -0,0 +1,28 @@
1
+ import type { GeneratorState } from './model.js';
2
+ export type AccountState = 'connected' | 'reconnect_needed' | 'not_connected';
3
+ export interface AccountSnapshot {
4
+ state: AccountState;
5
+ email?: string;
6
+ lastChecked?: string;
7
+ }
8
+ /** GeneratorState with dates as ISO strings, so it survives JSON. `lastExerciseAt` is the platform's persisted value (SPEC section 7). */
9
+ export type GeneratorSnapshot = Omit<GeneratorState, 'lastSeen' | 'lastExerciseAt'> & {
10
+ lastSeen: string | null;
11
+ lastExerciseAt: string | null;
12
+ };
13
+ export interface OtherDevice {
14
+ type: number;
15
+ name: string;
16
+ apparatusId: number;
17
+ }
18
+ export interface StateFile {
19
+ account: AccountSnapshot;
20
+ generators: GeneratorSnapshot[];
21
+ others: OtherDevice[];
22
+ updatedAt: string;
23
+ }
24
+ export declare function toGeneratorSnapshot(state: GeneratorState, lastExerciseAt?: string | null): GeneratorSnapshot;
25
+ export declare function buildStateFile(account: AccountSnapshot, generators: Iterable<GeneratorSnapshot>, others: Iterable<OtherDevice>, now?: Date): StateFile;
26
+ export declare function writeStateFile(file: string, state: StateFile): void;
27
+ /** The state file as last written, or null when missing or unreadable. Never throws. */
28
+ export declare function readStateFile(file: string): StateFile | null;
package/dist/state.js ADDED
@@ -0,0 +1,43 @@
1
+ /**
2
+ * The state file the platform writes after every poll and the settings page
3
+ * reads (SPEC section 10): <storagePath>/homebridge-generac/state.json.
4
+ */
5
+ import fs from 'node:fs';
6
+ import { writeFileAtomic } from './files.js';
7
+ export function toGeneratorSnapshot(state, lastExerciseAt) {
8
+ return {
9
+ ...state,
10
+ lastSeen: state.lastSeen ? state.lastSeen.toISOString() : null,
11
+ lastExerciseAt: lastExerciseAt !== undefined ? lastExerciseAt : state.lastExerciseAt ? state.lastExerciseAt.toISOString() : null,
12
+ };
13
+ }
14
+ export function buildStateFile(account, generators, others, now = new Date()) {
15
+ return {
16
+ account,
17
+ generators: [...generators],
18
+ others: [...others],
19
+ updatedAt: now.toISOString(),
20
+ };
21
+ }
22
+ export function writeStateFile(file, state) {
23
+ writeFileAtomic(file, JSON.stringify(state, null, 2) + '\n', 0o644);
24
+ }
25
+ /** The state file as last written, or null when missing or unreadable. Never throws. */
26
+ export function readStateFile(file) {
27
+ try {
28
+ const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
29
+ if (!parsed || typeof parsed !== 'object' || !parsed.account || typeof parsed.account !== 'object') {
30
+ return null;
31
+ }
32
+ return {
33
+ account: parsed.account,
34
+ generators: Array.isArray(parsed.generators) ? parsed.generators : [],
35
+ others: Array.isArray(parsed.others) ? parsed.others : [],
36
+ updatedAt: typeof parsed.updatedAt === 'string' ? parsed.updatedAt : '',
37
+ };
38
+ }
39
+ catch {
40
+ return null;
41
+ }
42
+ }
43
+ //# sourceMappingURL=state.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state.js","sourceRoot":"","sources":["../src/state.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AA2B7C,MAAM,UAAU,mBAAmB,CAAC,KAAqB,EAAE,cAA8B;IACvF,OAAO;QACL,GAAG,KAAK;QACR,QAAQ,EAAE,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,IAAI;QAC9D,cAAc,EAAE,cAAc,KAAK,SAAS,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC,CAAC,KAAK,CAAC,cAAc,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,IAAI;KACjI,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,cAAc,CAC5B,OAAwB,EACxB,UAAuC,EACvC,MAA6B,EAC7B,MAAY,IAAI,IAAI,EAAE;IAEtB,OAAO;QACL,OAAO;QACP,UAAU,EAAE,CAAC,GAAG,UAAU,CAAC;QAC3B,MAAM,EAAE,CAAC,GAAG,MAAM,CAAC;QACnB,SAAS,EAAE,GAAG,CAAC,WAAW,EAAE;KAC7B,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,IAAY,EAAE,KAAgB;IAC3D,eAAe,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,EAAE,KAAK,CAAC,CAAC;AACtE,CAAC;AAED,wFAAwF;AACxF,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAuB,CAAC;QAC/E,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,OAAO,IAAI,OAAO,MAAM,CAAC,OAAO,KAAK,QAAQ,EAAE,CAAC;YACnG,OAAO,IAAI,CAAC;QACd,CAAC;QACD,OAAO;YACL,OAAO,EAAE,MAAM,CAAC,OAAO;YACvB,UAAU,EAAE,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE;YACrE,MAAM,EAAE,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE;YACzD,SAAS,EAAE,OAAO,MAAM,CAAC,SAAS,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE;SACxE,CAAC;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC"}
@@ -0,0 +1,98 @@
1
+ /** Raw shapes from https://app.mobilelinkgen.com/api/v5, as observed September 15, 2026. */
2
+ export declare const DEVICE_TYPE: {
3
+ readonly GENERATOR: 0;
4
+ readonly UNKNOWN: 1;
5
+ readonly PROPANE_MONITOR: 2;
6
+ readonly LINKED_ECOBEE: 7;
7
+ };
8
+ export declare const DEVICE_TYPE_LABEL: Record<number, string>;
9
+ /** apparatusStatus enum. */
10
+ export declare const STATUS: {
11
+ readonly UNKNOWN0: 0;
12
+ readonly READY: 1;
13
+ readonly RUNNING: 2;
14
+ readonly EXERCISING: 3;
15
+ readonly WARNING: 4;
16
+ readonly STOPPED: 5;
17
+ readonly COMM_ISSUE: 6;
18
+ readonly UNKNOWN: 7;
19
+ };
20
+ export declare const STATUS_LABEL: Record<number, string>;
21
+ /**
22
+ * Numeric property.type values on /Apparatus/details. Stable across Generac's
23
+ * display-name changes, so key on these rather than on property.name.
24
+ */
25
+ export declare const PROP: {
26
+ readonly HOURS_OF_PROTECTION: 32;
27
+ readonly BATTERY_VOLTAGE: 70;
28
+ readonly ENGINE_HOURS: 71;
29
+ readonly FUEL_TYPE: 88;
30
+ readonly EXERCISE_MINUTES: 95;
31
+ };
32
+ export interface RawProperty {
33
+ name: string;
34
+ value: string | number | null;
35
+ type: number;
36
+ }
37
+ /** `alert.eventType` of the exercise-complete event (SPEC section 6). Observed once; see section 16 for the open item. */
38
+ export declare const EXERCISE_COMPLETE_EVENT = 42;
39
+ /** The `alert` object on /Apparatus/details: the most recent event on the unit. */
40
+ export interface RawAlert {
41
+ eCode?: number | null;
42
+ eventType?: number | null;
43
+ timestamp?: string | null;
44
+ type?: number | null;
45
+ }
46
+ export interface RawWeather {
47
+ temperature?: {
48
+ value: number;
49
+ unit: string;
50
+ unitType?: number;
51
+ };
52
+ iconCode?: number;
53
+ }
54
+ export interface RawApparatus {
55
+ apparatusId: number;
56
+ type: number;
57
+ name: string;
58
+ serialNumber: string | null;
59
+ modelNumber: string | null;
60
+ apparatusStatus: number;
61
+ isConnected: boolean;
62
+ showWarning: boolean;
63
+ properties?: RawProperty[];
64
+ weather?: RawWeather;
65
+ [k: string]: unknown;
66
+ }
67
+ export interface RawApparatusDetail {
68
+ apparatusId: number;
69
+ name: string;
70
+ serialNumber: string | null;
71
+ apparatusStatus: number;
72
+ statusLabel?: string | null;
73
+ statusText?: string | null;
74
+ isConnected: boolean;
75
+ isConnecting?: boolean;
76
+ showWarning: boolean;
77
+ hasMaintenanceAlert?: boolean;
78
+ serviceModeEnabled?: boolean;
79
+ lastSeen?: string | null;
80
+ deviceType?: string | null;
81
+ networkType?: string | null;
82
+ weather?: RawWeather;
83
+ properties?: RawProperty[];
84
+ tuProperties?: RawProperty[];
85
+ productInfo?: RawProperty[];
86
+ alarms?: unknown[];
87
+ warnings?: unknown[];
88
+ maintenance?: unknown[];
89
+ currentAlarm?: string | null;
90
+ alert?: RawAlert | null;
91
+ [k: string]: unknown;
92
+ }
93
+ export interface StoredCredentials {
94
+ email: string;
95
+ refresh_token: string;
96
+ dpop_private_key_pem: string;
97
+ created_at: string;
98
+ }
package/dist/types.js ADDED
@@ -0,0 +1,48 @@
1
+ /** Raw shapes from https://app.mobilelinkgen.com/api/v5, as observed September 15, 2026. */
2
+ export const DEVICE_TYPE = {
3
+ GENERATOR: 0,
4
+ UNKNOWN: 1,
5
+ PROPANE_MONITOR: 2,
6
+ LINKED_ECOBEE: 7,
7
+ };
8
+ export const DEVICE_TYPE_LABEL = {
9
+ 0: 'generator',
10
+ 1: 'unknown device',
11
+ 2: 'propane tank monitor',
12
+ 7: 'linked ecobee thermostat',
13
+ };
14
+ /** apparatusStatus enum. */
15
+ export const STATUS = {
16
+ UNKNOWN0: 0,
17
+ READY: 1,
18
+ RUNNING: 2,
19
+ EXERCISING: 3,
20
+ WARNING: 4,
21
+ STOPPED: 5,
22
+ COMM_ISSUE: 6,
23
+ UNKNOWN: 7,
24
+ };
25
+ export const STATUS_LABEL = {
26
+ 0: 'Unknown',
27
+ 1: 'Ready',
28
+ 2: 'Running',
29
+ 3: 'Exercising',
30
+ 4: 'Warning',
31
+ 5: 'Stopped',
32
+ 6: 'Communication Issue',
33
+ 7: 'Unknown',
34
+ };
35
+ /**
36
+ * Numeric property.type values on /Apparatus/details. Stable across Generac's
37
+ * display-name changes, so key on these rather than on property.name.
38
+ */
39
+ export const PROP = {
40
+ HOURS_OF_PROTECTION: 32,
41
+ BATTERY_VOLTAGE: 70,
42
+ ENGINE_HOURS: 71,
43
+ FUEL_TYPE: 88,
44
+ EXERCISE_MINUTES: 95,
45
+ };
46
+ /** `alert.eventType` of the exercise-complete event (SPEC section 6). Observed once; see section 16 for the open item. */
47
+ export const EXERCISE_COMPLETE_EVENT = 42;
48
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,4FAA4F;AAE5F,MAAM,CAAC,MAAM,WAAW,GAAG;IACzB,SAAS,EAAE,CAAC;IACZ,OAAO,EAAE,CAAC;IACV,eAAe,EAAE,CAAC;IAClB,aAAa,EAAE,CAAC;CACR,CAAC;AAEX,MAAM,CAAC,MAAM,iBAAiB,GAA2B;IACvD,CAAC,EAAE,WAAW;IACd,CAAC,EAAE,gBAAgB;IACnB,CAAC,EAAE,sBAAsB;IACzB,CAAC,EAAE,0BAA0B;CAC9B,CAAC;AAEF,4BAA4B;AAC5B,MAAM,CAAC,MAAM,MAAM,GAAG;IACpB,QAAQ,EAAE,CAAC;IACX,KAAK,EAAE,CAAC;IACR,OAAO,EAAE,CAAC;IACV,UAAU,EAAE,CAAC;IACb,OAAO,EAAE,CAAC;IACV,OAAO,EAAE,CAAC;IACV,UAAU,EAAE,CAAC;IACb,OAAO,EAAE,CAAC;CACF,CAAC;AAEX,MAAM,CAAC,MAAM,YAAY,GAA2B;IAClD,CAAC,EAAE,SAAS;IACZ,CAAC,EAAE,OAAO;IACV,CAAC,EAAE,SAAS;IACZ,CAAC,EAAE,YAAY;IACf,CAAC,EAAE,SAAS;IACZ,CAAC,EAAE,SAAS;IACZ,CAAC,EAAE,qBAAqB;IACxB,CAAC,EAAE,SAAS;CACb,CAAC;AAEF;;;GAGG;AACH,MAAM,CAAC,MAAM,IAAI,GAAG;IAClB,mBAAmB,EAAE,EAAE;IACvB,eAAe,EAAE,EAAE;IACnB,YAAY,EAAE,EAAE;IAChB,SAAS,EAAE,EAAE;IACb,gBAAgB,EAAE,EAAE;CACZ,CAAC;AAQX,0HAA0H;AAC1H,MAAM,CAAC,MAAM,uBAAuB,GAAG,EAAE,CAAC"}
@@ -0,0 +1,96 @@
1
+ import { HomebridgePluginUiServer } from '@homebridge/plugin-ui-utils';
2
+ import { type LoginOptions, type LoginResult, type MfaType } from '../auth.js';
3
+ import { type AccountSnapshot, type GeneratorSnapshot } from '../state.js';
4
+ /** A pending Connect expires this long after Sign in (SPEC section 10). */
5
+ export declare const PENDING_LOGIN_TTL_MS: number;
6
+ /** Three rejected codes end the sign-in (SPEC section 4.1). */
7
+ export declare const MAX_CODE_ATTEMPTS = 3;
8
+ export type LoginFn = (email: string, password: string, opts: LoginOptions) => Promise<LoginResult>;
9
+ export type StartError = 'wrong_password' | 'unknown_email' | 'unsupported_factor' | 'network';
10
+ export type CodeError = 'wrong_code' | 'too_many' | 'expired';
11
+ export type StartResponse = {
12
+ step: 'code';
13
+ method: MfaType;
14
+ } | {
15
+ step: 'done';
16
+ } | {
17
+ error: StartError;
18
+ };
19
+ export type CodeResponse = {
20
+ step: 'done';
21
+ } | {
22
+ error: CodeError;
23
+ };
24
+ export interface StatusAccount extends Omit<AccountSnapshot, 'state'> {
25
+ state: AccountSnapshot['state'] | 'checking';
26
+ }
27
+ export interface StatusResponse {
28
+ account: StatusAccount;
29
+ generators: GeneratorSnapshot[];
30
+ others: Array<{
31
+ type: number;
32
+ name: string;
33
+ }>;
34
+ /** The installed package version, for the page footer. */
35
+ version: string;
36
+ }
37
+ export interface UiServerOptions {
38
+ storagePath: string;
39
+ /** config.json, read only for `credentialsPath` (SPEC section 4.4). */
40
+ configPath?: string;
41
+ login?: LoginFn;
42
+ now?: () => number;
43
+ version?: string;
44
+ }
45
+ type Handler = (payload: unknown) => Promise<unknown>;
46
+ /** Maps a failed login to the page's error codes (SPEC section 10). */
47
+ export declare function mapError(err: unknown, phase: 'start' | 'code'): StartResponse | CodeResponse;
48
+ export declare class GeneracUiHandlers {
49
+ private readonly opts;
50
+ private readonly login;
51
+ private readonly now;
52
+ private readonly version;
53
+ private pending;
54
+ constructor(opts: UiServerOptions);
55
+ /** The request paths of SPEC section 10 and their handlers, for HomebridgePluginUiServer.onRequest. */
56
+ routes(): Record<string, Handler>;
57
+ status(): Promise<StatusResponse>;
58
+ /** True when the state file's account entry was written after these credentials were created. */
59
+ private stateNewerThan;
60
+ connectStart(payload: unknown): Promise<StartResponse>;
61
+ connectCode(payload: unknown): Promise<CodeResponse>;
62
+ connectCancel(): Promise<{
63
+ ok: true;
64
+ }>;
65
+ /** Whether a Connect is in progress (for tests). */
66
+ get hasPending(): boolean;
67
+ private settle;
68
+ /** The login ended (success or failure): forget it. */
69
+ private finish;
70
+ /** Cancel, replace or expire: the login is abandoned, its result (if it ever arrives) ignored, its prompt rejected. */
71
+ private discardPending;
72
+ private expire;
73
+ private writeCredentials;
74
+ disconnect(): Promise<{
75
+ ok: true;
76
+ }>;
77
+ /**
78
+ * The Reset dialog (SPEC section 11.3 E): sign out, forget the saved state, and leave a marker so the platform
79
+ * removes every cached accessory on its next start. The page resets the platform block itself.
80
+ */
81
+ reset(): Promise<{
82
+ ok: true;
83
+ }>;
84
+ private deleteCredentials;
85
+ /** Clears the account section of state.json, keeping the last generator states for the page. */
86
+ private clearAccountState;
87
+ private findCredentials;
88
+ /** `credentialsPath` from the platform block in config.json, when set. */
89
+ private configuredCredentialsPath;
90
+ }
91
+ /** The process the Homebridge UI starts (homebridge-ui/server.js). */
92
+ export declare class GeneracUiServer extends HomebridgePluginUiServer {
93
+ constructor();
94
+ }
95
+ export declare function startUiServer(): GeneracUiServer;
96
+ export {};