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,42 @@
1
+ /** "HH:MM" to minutes past midnight, or null when malformed. */
2
+ export declare function parseHHMM(value: string | null | undefined): number | null;
3
+ /** The watch window opens this many minutes before the exercise time and closes this many after it. */
4
+ export declare const WATCH_BEFORE_MINUTES = 2;
5
+ export declare const WATCH_AFTER_MINUTES = 20;
6
+ /**
7
+ * True from `exerciseMinutes - 2` (inclusive) to `exerciseMinutes + 20` (exclusive), every day, in the host's
8
+ * local time zone. A window that straddles midnight (an exercise at 00:01 or 23:50) is handled.
9
+ */
10
+ export declare function inWatchWindow(exerciseMinutes: number, now: Date): boolean;
11
+ /** Milliseconds until the next watch window opens; 0 while inside one. */
12
+ export declare function msUntilWatchWindow(exerciseMinutes: number, now: Date): number;
13
+ export interface ExerciseObservation {
14
+ /** Whether the Exercising sensor should be open after this observation. */
15
+ open: boolean;
16
+ /** True when this observation triggered the sensor (live status 3, or a newer lastExerciseAt). */
17
+ triggered: boolean;
18
+ /** True when lastExerciseAt advanced past the persisted value (the retroactive path). */
19
+ retroactive: boolean;
20
+ }
21
+ /**
22
+ * The Exercising sensor's state for one generator (SPEC section 7, service 6).
23
+ *
24
+ * The sensor opens when a poll observes status 3, or when `lastExerciseAt` moves to a newer value than the
25
+ * one persisted in state.json. It stays open until the status has left 3 and the hold has elapsed since the
26
+ * last trigger. On first run (nothing persisted) the current `lastExerciseAt` is recorded without opening.
27
+ */
28
+ export declare class ExerciseTracker {
29
+ private readonly holdMs;
30
+ /** The newest lastExerciseAt seen, as epoch ms; null when the unit has never reported one; undefined before the first observation. */
31
+ private recorded;
32
+ private lastTriggerAt;
33
+ private exercisingNow;
34
+ constructor(persisted: string | null | undefined, holdMs: number);
35
+ /** The value to persist in state.json: the newest lastExerciseAt seen, as an ISO string. */
36
+ get lastExerciseAt(): string | null;
37
+ observe(status: number, lastExerciseAt: Date | null, now: number): ExerciseObservation;
38
+ /** Whether the sensor is open at `now`: exercising, or within the hold since the last trigger. */
39
+ isOpen(now: number): boolean;
40
+ /** Milliseconds until the hold expires, or null when nothing is held (or the status still keeps it open). */
41
+ holdRemaining(now: number): number | null;
42
+ }
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Exercise detection and the watch window (SPEC sections 7 and 8). Pure: no timers, no HAP, so the
3
+ * platform tests drive it with fixed clocks.
4
+ */
5
+ import { STATUS } from './types.js';
6
+ /** "HH:MM" to minutes past midnight, or null when malformed. */
7
+ export function parseHHMM(value) {
8
+ if (!value) {
9
+ return null;
10
+ }
11
+ const m = /^(\d{1,2}):(\d{2})$/.exec(value.trim());
12
+ if (!m) {
13
+ return null;
14
+ }
15
+ const h = Number(m[1]);
16
+ const min = Number(m[2]);
17
+ if (h > 23 || min > 59) {
18
+ return null;
19
+ }
20
+ return h * 60 + min;
21
+ }
22
+ /** The watch window opens this many minutes before the exercise time and closes this many after it. */
23
+ export const WATCH_BEFORE_MINUTES = 2;
24
+ export const WATCH_AFTER_MINUTES = 20;
25
+ const DAY_MINUTES = 24 * 60;
26
+ /** Minutes past local midnight of `now`, with the seconds as a fraction. */
27
+ function localMinutes(now) {
28
+ return now.getHours() * 60 + now.getMinutes() + now.getSeconds() / 60 + now.getMilliseconds() / 60000;
29
+ }
30
+ /**
31
+ * True from `exerciseMinutes - 2` (inclusive) to `exerciseMinutes + 20` (exclusive), every day, in the host's
32
+ * local time zone. A window that straddles midnight (an exercise at 00:01 or 23:50) is handled.
33
+ */
34
+ export function inWatchWindow(exerciseMinutes, now) {
35
+ const offset = (localMinutes(now) - (exerciseMinutes - WATCH_BEFORE_MINUTES) + DAY_MINUTES) % DAY_MINUTES;
36
+ return offset < WATCH_BEFORE_MINUTES + WATCH_AFTER_MINUTES;
37
+ }
38
+ /** Milliseconds until the next watch window opens; 0 while inside one. */
39
+ export function msUntilWatchWindow(exerciseMinutes, now) {
40
+ if (inWatchWindow(exerciseMinutes, now)) {
41
+ return 0;
42
+ }
43
+ const start = exerciseMinutes - WATCH_BEFORE_MINUTES;
44
+ const until = (start - localMinutes(now) + DAY_MINUTES) % DAY_MINUTES;
45
+ return Math.max(1, Math.round(until * 60000));
46
+ }
47
+ /**
48
+ * The Exercising sensor's state for one generator (SPEC section 7, service 6).
49
+ *
50
+ * The sensor opens when a poll observes status 3, or when `lastExerciseAt` moves to a newer value than the
51
+ * one persisted in state.json. It stays open until the status has left 3 and the hold has elapsed since the
52
+ * last trigger. On first run (nothing persisted) the current `lastExerciseAt` is recorded without opening.
53
+ */
54
+ export class ExerciseTracker {
55
+ holdMs;
56
+ /** The newest lastExerciseAt seen, as epoch ms; null when the unit has never reported one; undefined before the first observation. */
57
+ recorded;
58
+ lastTriggerAt = null;
59
+ exercisingNow = false;
60
+ constructor(persisted, holdMs) {
61
+ this.holdMs = holdMs;
62
+ if (persisted === undefined) {
63
+ this.recorded = undefined;
64
+ }
65
+ else if (persisted === null) {
66
+ this.recorded = null;
67
+ }
68
+ else {
69
+ const t = Date.parse(persisted);
70
+ this.recorded = Number.isNaN(t) ? null : t;
71
+ }
72
+ }
73
+ /** The value to persist in state.json: the newest lastExerciseAt seen, as an ISO string. */
74
+ get lastExerciseAt() {
75
+ return typeof this.recorded === 'number' ? new Date(this.recorded).toISOString() : null;
76
+ }
77
+ observe(status, lastExerciseAt, now) {
78
+ let triggered = false;
79
+ let retroactive = false;
80
+ this.exercisingNow = status === STATUS.EXERCISING;
81
+ if (this.exercisingNow) {
82
+ triggered = true;
83
+ }
84
+ const seen = lastExerciseAt ? lastExerciseAt.getTime() : null;
85
+ if (this.recorded === undefined) {
86
+ // First run: remember what the unit reports without opening the sensor for an exercise that may be days old.
87
+ this.recorded = seen;
88
+ }
89
+ else if (seen !== null && (this.recorded === null || seen > this.recorded)) {
90
+ this.recorded = seen;
91
+ triggered = true;
92
+ retroactive = true;
93
+ }
94
+ if (triggered) {
95
+ this.lastTriggerAt = now;
96
+ }
97
+ return { open: this.isOpen(now), triggered, retroactive };
98
+ }
99
+ /** Whether the sensor is open at `now`: exercising, or within the hold since the last trigger. */
100
+ isOpen(now) {
101
+ if (this.exercisingNow) {
102
+ return true;
103
+ }
104
+ return this.lastTriggerAt !== null && now - this.lastTriggerAt < this.holdMs;
105
+ }
106
+ /** Milliseconds until the hold expires, or null when nothing is held (or the status still keeps it open). */
107
+ holdRemaining(now) {
108
+ if (this.lastTriggerAt === null) {
109
+ return null;
110
+ }
111
+ const remaining = this.lastTriggerAt + this.holdMs - now;
112
+ return remaining > 0 ? remaining : null;
113
+ }
114
+ }
115
+ //# sourceMappingURL=exercise.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"exercise.js","sourceRoot":"","sources":["../src/exercise.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Write a file atomically: write to a temp file in the same directory, fsync,
3
+ * then rename over the target. A reader never sees a half-written file.
4
+ */
5
+ export declare function writeFileAtomic(file: string, data: string, mode: number): void;
package/dist/files.js ADDED
@@ -0,0 +1,28 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ /**
4
+ * Write a file atomically: write to a temp file in the same directory, fsync,
5
+ * then rename over the target. A reader never sees a half-written file.
6
+ */
7
+ export function writeFileAtomic(file, data, mode) {
8
+ const dir = path.dirname(file);
9
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
10
+ const tmp = path.join(dir, `.${path.basename(file)}.${process.pid}.${Date.now()}.tmp`);
11
+ const fd = fs.openSync(tmp, 'w', mode);
12
+ try {
13
+ fs.writeSync(fd, data);
14
+ fs.fsyncSync(fd);
15
+ }
16
+ finally {
17
+ fs.closeSync(fd);
18
+ }
19
+ try {
20
+ fs.chmodSync(tmp, mode);
21
+ fs.renameSync(tmp, file);
22
+ }
23
+ catch (err) {
24
+ fs.rmSync(tmp, { force: true });
25
+ throw err;
26
+ }
27
+ }
28
+ //# sourceMappingURL=files.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"files.js","sourceRoot":"","sources":["../src/files.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,IAAY,EAAE,IAAY,EAAE,IAAY;IACtE,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/B,EAAE,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IACpD,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,OAAO,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;IACvF,MAAM,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC;IACvC,IAAI,CAAC;QACH,EAAE,CAAC,SAAS,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;QACvB,EAAE,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;IACnB,CAAC;YAAS,CAAC;QACT,EAAE,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;IACnB,CAAC;IACD,IAAI,CAAC;QACH,EAAE,CAAC,SAAS,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QACxB,EAAE,CAAC,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;IAC3B,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,EAAE,CAAC,MAAM,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QAChC,MAAM,GAAG,CAAC;IACZ,CAAC;AACH,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { API } from 'homebridge';
2
+ declare const _default: (api: API) => void;
3
+ export default _default;
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ import { GeneracPlatform } from './platform.js';
2
+ import { PLATFORM_NAME } from './settings.js';
3
+ export default (api) => {
4
+ api.registerPlatform(PLATFORM_NAME, GeneracPlatform);
5
+ };
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAE9C,eAAe,CAAC,GAAQ,EAAQ,EAAE;IAChC,GAAG,CAAC,gBAAgB,CAAC,aAAa,EAAE,eAAe,CAAC,CAAC;AACvD,CAAC,CAAC"}
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Pure mapping from Generac's raw payloads to the state the accessory layer
3
+ * consumes. No I/O, no HAP, so it is fully unit-testable against fixtures.
4
+ */
5
+ import { type RawApparatus, type RawApparatusDetail, type RawProperty } from './types.js';
6
+ export interface FaultOptions {
7
+ faultOnStopped: boolean;
8
+ faultOnDisconnected: boolean;
9
+ }
10
+ export interface GeneratorState {
11
+ id: number;
12
+ name: string;
13
+ serial: string;
14
+ model: string;
15
+ status: number;
16
+ statusLabel: string;
17
+ statusText: string;
18
+ running: boolean;
19
+ exercising: boolean;
20
+ connected: boolean;
21
+ fault: boolean;
22
+ faultReasons: string[];
23
+ maintenanceDue: boolean;
24
+ batteryVoltage: number | null;
25
+ engineHours: number | null;
26
+ hoursOfProtection: number | null;
27
+ /** The API's Exercise Minutes as a 12-hour label ("10:05 AM"), for the log and the CLI. */
28
+ exerciseTime: string | null;
29
+ /** The API's Exercise Minutes as 24-hour "HH:MM", the default for the `exerciseTime` setting (SPEC section 9). */
30
+ exerciseTimeFromApi: string | null;
31
+ /** When the last exercise finished, from the eventType 42 alert (SPEC section 6); null when the payload carries another event. */
32
+ lastExerciseAt: Date | null;
33
+ fuelType: string | null;
34
+ outdoorTempF: number | null;
35
+ lastSeen: Date | null;
36
+ }
37
+ export declare function propByType(props: RawProperty[] | undefined, type: number): RawProperty | undefined;
38
+ /** Generac mixes "13.6" and 17 in the same array. */
39
+ export declare function numericProp(props: RawProperty[] | undefined, type: number): number | null;
40
+ export declare function fuelTypeLabel(props: RawProperty[] | undefined): string | null;
41
+ /** Exercise Minutes is minutes past midnight (605 = 10:05 AM); null when absent or out of range. */
42
+ export declare function exerciseMinutes(props: RawProperty[] | undefined): number | null;
43
+ /** Minutes past midnight as a 12-hour label: 605 becomes "10:05 AM". */
44
+ export declare function minutesToLabel(m: number): string;
45
+ /** Minutes past midnight as 24-hour "HH:MM": 605 becomes "10:05". */
46
+ export declare function minutesToHHMM(m: number): string;
47
+ export declare function exerciseTimeLabel(props: RawProperty[] | undefined): string | null;
48
+ /**
49
+ * The last exercise, from the details payload's `alert` (SPEC section 6): its timestamp when the event type
50
+ * is the exercise-complete event, else null. Any other event (an alarm, a maintenance note) says nothing
51
+ * about exercise, so the previous value is kept by the caller rather than cleared here.
52
+ */
53
+ export declare function lastExerciseFromAlert(detail: RawApparatusDetail): Date | null;
54
+ /**
55
+ * User-facing fault reasons (SPEC section 11.3 D). Shared by the log line and
56
+ * the settings page so both show the same words.
57
+ */
58
+ export declare const FAULT_REASON: {
59
+ readonly SWITCH_OFF: "Switch in OFF";
60
+ readonly WARNING_STATUS: "Warning status";
61
+ readonly LOST_CONNECTION: "Lost connection";
62
+ readonly activeAlarms: (n: number) => string;
63
+ readonly activeWarnings: (n: number) => string;
64
+ readonly alarmCode: (code: string) => string;
65
+ };
66
+ export declare function modelFromDetail(detail: RawApparatusDetail, list?: RawApparatus): string;
67
+ export declare function toGeneratorState(detail: RawApparatusDetail, list: RawApparatus | undefined, opts: FaultOptions): GeneratorState;
68
+ /**
69
+ * Lead-acid starting battery, approximate state of charge from resting or float
70
+ * voltage. Clamped: a healthy unit on float charge reads 13.4 to 13.8V.
71
+ */
72
+ export declare function batteryPercent(volts: number | null): number;
73
+ /** True when a generator is in a state worth polling faster for. */
74
+ export declare function isActive(state: GeneratorState): boolean;
package/dist/model.js ADDED
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Pure mapping from Generac's raw payloads to the state the accessory layer
3
+ * consumes. No I/O, no HAP, so it is fully unit-testable against fixtures.
4
+ */
5
+ import { EXERCISE_COMPLETE_EVENT, PROP, STATUS, STATUS_LABEL, } from './types.js';
6
+ export function propByType(props, type) {
7
+ return props?.find((p) => p.type === type);
8
+ }
9
+ /** Generac mixes "13.6" and 17 in the same array. */
10
+ export function numericProp(props, type) {
11
+ const p = propByType(props, type);
12
+ if (!p || p.value === null || p.value === undefined || p.value === '') {
13
+ return null;
14
+ }
15
+ const n = typeof p.value === 'number' ? p.value : Number(p.value);
16
+ return Number.isFinite(n) ? n : null;
17
+ }
18
+ const FUEL_TYPE_LABEL = {
19
+ '1': 'natural gas',
20
+ '2': 'propane',
21
+ '3': 'diesel',
22
+ };
23
+ export function fuelTypeLabel(props) {
24
+ const p = propByType(props, PROP.FUEL_TYPE);
25
+ if (!p || p.value === null || p.value === undefined) {
26
+ return null;
27
+ }
28
+ const key = String(p.value);
29
+ return FUEL_TYPE_LABEL[key] ?? `fuel type ${key}`;
30
+ }
31
+ /** Exercise Minutes is minutes past midnight (605 = 10:05 AM); null when absent or out of range. */
32
+ export function exerciseMinutes(props) {
33
+ const m = numericProp(props, PROP.EXERCISE_MINUTES);
34
+ if (m === null || m < 0 || m >= 1440) {
35
+ return null;
36
+ }
37
+ return Math.floor(m);
38
+ }
39
+ /** Minutes past midnight as a 12-hour label: 605 becomes "10:05 AM". */
40
+ export function minutesToLabel(m) {
41
+ const h24 = Math.floor(m / 60);
42
+ const mm = String(m % 60).padStart(2, '0');
43
+ const h12 = h24 % 12 === 0 ? 12 : h24 % 12;
44
+ return `${h12}:${mm} ${h24 < 12 ? 'AM' : 'PM'}`;
45
+ }
46
+ /** Minutes past midnight as 24-hour "HH:MM": 605 becomes "10:05". */
47
+ export function minutesToHHMM(m) {
48
+ return `${String(Math.floor(m / 60)).padStart(2, '0')}:${String(m % 60).padStart(2, '0')}`;
49
+ }
50
+ export function exerciseTimeLabel(props) {
51
+ const m = exerciseMinutes(props);
52
+ return m === null ? null : minutesToLabel(m);
53
+ }
54
+ /**
55
+ * The last exercise, from the details payload's `alert` (SPEC section 6): its timestamp when the event type
56
+ * is the exercise-complete event, else null. Any other event (an alarm, a maintenance note) says nothing
57
+ * about exercise, so the previous value is kept by the caller rather than cleared here.
58
+ */
59
+ export function lastExerciseFromAlert(detail) {
60
+ const alert = detail.alert;
61
+ if (!alert || alert.eventType !== EXERCISE_COMPLETE_EVENT || !alert.timestamp) {
62
+ return null;
63
+ }
64
+ const d = new Date(alert.timestamp);
65
+ return Number.isNaN(d.getTime()) ? null : d;
66
+ }
67
+ /**
68
+ * User-facing fault reasons (SPEC section 11.3 D). Shared by the log line and
69
+ * the settings page so both show the same words.
70
+ */
71
+ export const FAULT_REASON = {
72
+ SWITCH_OFF: 'Switch in OFF',
73
+ WARNING_STATUS: 'Warning status',
74
+ LOST_CONNECTION: 'Lost connection',
75
+ activeAlarms: (n) => `${n} active alarm(s)`,
76
+ activeWarnings: (n) => `${n} active warning(s)`,
77
+ alarmCode: (code) => `Alarm code ${code}`,
78
+ };
79
+ export function modelFromDetail(detail, list) {
80
+ const desc = detail.productInfo?.find((p) => p.name === 'Description')?.value;
81
+ if (typeof desc === 'string' && desc.trim()) {
82
+ return desc.trim();
83
+ }
84
+ return list?.modelNumber ?? 'Generac generator';
85
+ }
86
+ export function toGeneratorState(detail, list, opts) {
87
+ const status = detail.apparatusStatus ?? list?.apparatusStatus ?? STATUS.UNKNOWN;
88
+ const connected = detail.isConnected ?? list?.isConnected ?? false;
89
+ const reasons = [];
90
+ const reason = (r) => {
91
+ if (!reasons.includes(r)) {
92
+ reasons.push(r);
93
+ }
94
+ };
95
+ if (status === STATUS.WARNING) {
96
+ reason(FAULT_REASON.WARNING_STATUS);
97
+ }
98
+ if (status === STATUS.STOPPED && opts.faultOnStopped) {
99
+ reason(FAULT_REASON.SWITCH_OFF);
100
+ }
101
+ if (status === STATUS.COMM_ISSUE && opts.faultOnDisconnected) {
102
+ reason(FAULT_REASON.LOST_CONNECTION);
103
+ }
104
+ if (!connected && opts.faultOnDisconnected) {
105
+ reason(FAULT_REASON.LOST_CONNECTION);
106
+ }
107
+ if (detail.showWarning) {
108
+ reason(FAULT_REASON.WARNING_STATUS);
109
+ }
110
+ const alarms = detail.alarms?.length ?? 0;
111
+ if (alarms > 0) {
112
+ reason(FAULT_REASON.activeAlarms(alarms));
113
+ }
114
+ const warnings = detail.warnings?.length ?? 0;
115
+ if (warnings > 0) {
116
+ reason(FAULT_REASON.activeWarnings(warnings));
117
+ }
118
+ if (detail.currentAlarm && detail.currentAlarm !== '0') {
119
+ reason(FAULT_REASON.alarmCode(detail.currentAlarm));
120
+ }
121
+ const minutes = exerciseMinutes(detail.properties);
122
+ const temp = detail.weather?.temperature;
123
+ let outdoorTempF = null;
124
+ if (temp && Number.isFinite(temp.value)) {
125
+ outdoorTempF = temp.unit?.toUpperCase() === 'C' ? (temp.value * 9) / 5 + 32 : temp.value;
126
+ }
127
+ return {
128
+ id: detail.apparatusId,
129
+ name: detail.name || list?.name || 'Generator',
130
+ serial: detail.serialNumber || list?.serialNumber || String(detail.apparatusId),
131
+ model: modelFromDetail(detail, list),
132
+ status,
133
+ statusLabel: detail.statusLabel || STATUS_LABEL[status] || 'Unknown',
134
+ statusText: detail.statusText || '',
135
+ running: status === STATUS.RUNNING,
136
+ exercising: status === STATUS.EXERCISING,
137
+ connected,
138
+ fault: reasons.length > 0,
139
+ faultReasons: reasons,
140
+ maintenanceDue: Boolean(detail.hasMaintenanceAlert) || (detail.maintenance?.length ?? 0) > 0,
141
+ batteryVoltage: numericProp(detail.properties, PROP.BATTERY_VOLTAGE),
142
+ engineHours: numericProp(detail.properties, PROP.ENGINE_HOURS),
143
+ hoursOfProtection: numericProp(detail.properties, PROP.HOURS_OF_PROTECTION),
144
+ exerciseTime: exerciseTimeLabel(detail.properties),
145
+ exerciseTimeFromApi: minutes === null ? null : minutesToHHMM(minutes),
146
+ lastExerciseAt: lastExerciseFromAlert(detail),
147
+ fuelType: fuelTypeLabel(detail.properties),
148
+ outdoorTempF,
149
+ lastSeen: detail.lastSeen ? new Date(detail.lastSeen) : null,
150
+ };
151
+ }
152
+ /**
153
+ * Lead-acid starting battery, approximate state of charge from resting or float
154
+ * voltage. Clamped: a healthy unit on float charge reads 13.4 to 13.8V.
155
+ */
156
+ export function batteryPercent(volts) {
157
+ if (volts === null) {
158
+ return 100;
159
+ }
160
+ const pct = ((volts - 11.8) / (12.8 - 11.8)) * 100;
161
+ return Math.max(0, Math.min(100, Math.round(pct)));
162
+ }
163
+ /** True when a generator is in a state worth polling faster for. */
164
+ export function isActive(state) {
165
+ return state.running || state.exercising || state.fault;
166
+ }
167
+ //# sourceMappingURL=model.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"model.js","sourceRoot":"","sources":["../src/model.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,117 @@
1
+ import type { API, DynamicPlatformPlugin, Logger, PlatformAccessory, PlatformConfig } from 'homebridge';
2
+ import { type GeneratorState } from './model.js';
3
+ import { type ResolvedConfig } from './settings.js';
4
+ import { type AccountState } from './state.js';
5
+ /** How often the credentials file is re-read (SPEC section 4.4): for a sign-in while there is no working client, and for a Disconnect while there is one. */
6
+ export declare const CREDENTIAL_CHECK_MS: number;
7
+ /** Retry interval after `invalid_grant` (SPEC section 4.3). */
8
+ export declare const RECONNECT_RETRY_MS: number;
9
+ /** Backoff cap for other poll failures (SPEC section 8). */
10
+ export declare const BACKOFF_MAX_MS: number;
11
+ export declare class GeneracPlatform implements DynamicPlatformPlugin {
12
+ readonly log: Logger;
13
+ readonly api: API;
14
+ readonly config: ResolvedConfig;
15
+ readonly version: string;
16
+ /** What the accessories report as FirmwareRevision (SPEC section 7). */
17
+ readonly firmware: string;
18
+ private readonly storagePath;
19
+ private readonly credentialsPath;
20
+ private readonly attentionUuid;
21
+ private readonly cached;
22
+ private readonly generators;
23
+ /** Exercise detection per generator (SPEC section 7), seeded from state.json on start. */
24
+ private readonly trackers;
25
+ private readonly holdTimers;
26
+ /** `lastExerciseAt` per generator as state.json held it on start; a missing entry means first run for that unit. */
27
+ private readonly persistedExercise;
28
+ private others;
29
+ private attention;
30
+ private client;
31
+ private credentials;
32
+ private accountState;
33
+ private lastChecked;
34
+ private reconnectLogged;
35
+ private pollTimer;
36
+ private credentialTimer;
37
+ private inFlight;
38
+ private failures;
39
+ private stopped;
40
+ /** Delay of the most recently scheduled poll in milliseconds, for the debug log and tests. */
41
+ nextPollMs: number | null;
42
+ /** The clock, replaceable by tests (the hold timer and the watch window read it). */
43
+ now: () => number;
44
+ constructor(log: Logger, rawConfig: PlatformConfig, api: API);
45
+ configureAccessory(accessory: PlatformAccessory): void;
46
+ /** Account state as the settings page sees it. */
47
+ get account(): AccountState;
48
+ /**
49
+ * Runs once Homebridge has restored cached accessories. Public so tests can
50
+ * await the first poll.
51
+ */
52
+ start(): Promise<void>;
53
+ shutdown(): void;
54
+ /**
55
+ * The settings page's Reset dialog leaves a marker (SPEC section 11.3 E, "Removes every generator and its sensors
56
+ * from the Home app"): every cached accessory goes on this start, once, and the marker with it.
57
+ */
58
+ private applyResetMarker;
59
+ private setupAttention;
60
+ private findCredentials;
61
+ private static signatureOf;
62
+ /**
63
+ * Load credentials when a usable file exists and differs from what is loaded
64
+ * (compared by `created_at`). Returns true when a new client was created.
65
+ */
66
+ private loadCredentials;
67
+ private scheduleCredentialCheck;
68
+ /**
69
+ * Re-read the credentials file. When a new or changed file is usable, poll
70
+ * immediately so a sign-in takes effect without a restart; when the file the
71
+ * client came from is gone (Disconnect), stop. Runs every 60 s. Public for tests.
72
+ */
73
+ checkCredentials(): Promise<void>;
74
+ /**
75
+ * The credentials file disappeared (the settings page's Disconnect, or a hand delete): stop polling, drop the
76
+ * client, report not_connected, mark the sensors not responding, and keep checking the file every 60 s.
77
+ */
78
+ private credentialsRemoved;
79
+ private schedule;
80
+ /** One poll: /Apparatus/list, then details for each generator. Public for tests. */
81
+ poll(): Promise<void>;
82
+ private doPoll;
83
+ /**
84
+ * The delay until the next poll (SPEC section 8): the active interval while any generator is active or the
85
+ * exercise watch window is open, else the idle interval, cut short so the next poll lands when the window opens.
86
+ */
87
+ private nextInterval;
88
+ /**
89
+ * The exercise time the watch window follows, as minutes past local midnight: the `exerciseTime` setting,
90
+ * else the first generator's Exercise Minutes from the API, else null (no window).
91
+ */
92
+ exerciseMinutes(): number | null;
93
+ /** Returns true when the generator is in an "active" state that warrants faster polling. */
94
+ private syncGenerator;
95
+ /** A debug capture of the raw payload (SPEC section 12). Never blocks a poll: a write failure is warned about once. */
96
+ private capture;
97
+ private loadPersistedExercise;
98
+ /** Feeds one poll into the generator's tracker and drives the sensor. Returns true on a retroactive detection. */
99
+ private observeExercise;
100
+ /** Closes the sensor when the hold runs out between polls. */
101
+ private scheduleHoldExpiry;
102
+ /** Re-evaluates every Exercising sensor against the clock. Public for tests, which drive the clock by hand. */
103
+ refreshExerciseSensors(): void;
104
+ /** The last state of every generator, for the state file and tests. */
105
+ get generatorStates(): GeneratorState[];
106
+ /** The generator states as the state file carries them, with the persisted `lastExerciseAt` per unit. */
107
+ private snapshots;
108
+ private writeState;
109
+ /**
110
+ * Debug lines. Sent through log.debug so Homebridge's own -D shows them;
111
+ * with the plugin's `debug` setting on they are raised to info so they show
112
+ * without -D, since Homebridge drops log.debug output otherwise.
113
+ */
114
+ private debug;
115
+ private readonly onceKeys;
116
+ private once;
117
+ }