homebridge-eggtimer-plugin 1.0.30

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.
@@ -0,0 +1,3 @@
1
+ # These are supported funding model platforms
2
+
3
+ github: teh-hippo
@@ -0,0 +1,19 @@
1
+ # To get started with Dependabot version updates, you'll need to specify which
2
+ # package ecosystems to update and where the package manifests are located.
3
+ # Please see the documentation for all configuration options:
4
+ # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5
+
6
+ version: 2
7
+ updates:
8
+
9
+ # Maintain dependencies for GitHub Actions
10
+ - package-ecosystem: "github-actions"
11
+ directory: "/"
12
+ schedule:
13
+ interval: "weekly"
14
+
15
+ # Maintain dependencies for npm
16
+ - package-ecosystem: "pnpm"
17
+ directory: "/"
18
+ schedule:
19
+ interval: "weekly"
@@ -0,0 +1,30 @@
1
+ # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
2
+ # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
3
+
4
+ name: Audit
5
+
6
+ on:
7
+
8
+ schedule:
9
+ - cron: '0 0 * * *'
10
+ workflow_dispatch:
11
+
12
+ jobs:
13
+ build:
14
+
15
+ runs-on: ubuntu-latest
16
+
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+ - uses: actions/cache@v4
20
+ name: Cache node modules
21
+ with:
22
+ path: node_modules
23
+ key: ${{ runner.os }}-node-${{ hashFiles('**/pnpm-lock.yaml') }}
24
+ - name: Use Node.js 20.x
25
+ uses: actions/setup-node@v4.0.2
26
+ with:
27
+ node-version: 20.x
28
+ - uses: pnpm/action-setup@v3
29
+ - run: pnpm install --frozen-lockfile
30
+ - run: pnpm audit
@@ -0,0 +1,16 @@
1
+ name: Auto-merge Dependabot
2
+ on: pull_request
3
+
4
+ permissions:
5
+ pull-requests: write
6
+ contents: write
7
+
8
+ jobs:
9
+ automerge:
10
+ runs-on: ubuntu-latest
11
+ if: github.actor == 'dependabot[bot]' || (startsWith(github.event.pull_request.title, 'Bump to version ') && (github.actor == 'github-actions' || github.actor == 'teh-hippo'))
12
+ steps:
13
+ - uses: peter-evans/enable-pull-request-automerge@v3
14
+ with:
15
+ pull-request-number: ${{ github.event.pull_request.number }}
16
+ merge-method: rebase
@@ -0,0 +1,32 @@
1
+ # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
2
+ # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
3
+
4
+ name: Build
5
+
6
+ on:
7
+ pull_request:
8
+
9
+ jobs:
10
+
11
+ build:
12
+
13
+ runs-on: ubuntu-latest
14
+
15
+ strategy:
16
+ matrix:
17
+ node-version: [16.x, 18.x, 20.x]
18
+
19
+ steps:
20
+ - uses: actions/checkout@v4
21
+ - uses: actions/cache@v4
22
+ name: Cache node modules
23
+ with:
24
+ path: node_modules
25
+ key: ${{ runner.os }}-node-${{ hashFiles('**/pnpm-lock.yaml') }}
26
+ - name: Use Node.js ${{ matrix.node-version }}
27
+ uses: actions/setup-node@v4.0.2
28
+ with:
29
+ node-version: ${{ matrix.node-version }}
30
+ - uses: pnpm/action-setup@v3
31
+ - run: pnpm install --frozen-lockfile
32
+ - run: pnpm lint
@@ -0,0 +1,44 @@
1
+ # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
2
+ # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
3
+
4
+ name: Update PNPM
5
+
6
+ on:
7
+
8
+ schedule:
9
+ - cron: '0 0 * * *'
10
+ workflow_dispatch:
11
+
12
+ permissions:
13
+ contents: write
14
+ pull-requests: write
15
+
16
+ jobs:
17
+ build:
18
+
19
+ runs-on: ubuntu-latest
20
+
21
+ steps:
22
+ - uses: actions/checkout@v4
23
+ - uses: actions/cache@v4
24
+ name: Cache node modules
25
+ with:
26
+ path: node_modules
27
+ key: ${{ runner.os }}-node-${{ hashFiles('**/pnpm-lock.json') }}
28
+ - name: Use Node.js 20.x
29
+ uses: actions/setup-node@v4.0.2
30
+ with:
31
+ node-version: 20.x
32
+ - uses: pnpm/action-setup@v3
33
+ - run: corepack up
34
+ - name: Create Pull Request
35
+ uses: peter-evans/create-pull-request@v6
36
+ with:
37
+ reviewers: teh-hippo
38
+ delete-branch: true
39
+ title: Update PNPM
40
+ commit-message: |
41
+ Update to the latest PNPM manager.
42
+ token: ${{ secrets.GITHUB_TOKEN }}
43
+ committer: AutoHippo <auto@hippo.org>
44
+ branch: autohippo/update-pnpm-${{ github.run_number }}
@@ -0,0 +1,57 @@
1
+ # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created
2
+ # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages
3
+
4
+ name: Release
5
+
6
+ on:
7
+ release:
8
+ types: [published]
9
+
10
+ jobs:
11
+ publish:
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ - uses: actions/setup-node@v4.0.2
16
+ with:
17
+ node-version: 18
18
+ registry-url: https://registry.npmjs.org/
19
+ - uses: pnpm/action-setup@v3
20
+ - run: pnpm install --frozen-lockfile
21
+ - run: pnpm build
22
+ - run: pnpm publish --no-git-checks
23
+ env:
24
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
25
+
26
+ update-build-number:
27
+ needs: publish
28
+ runs-on: ubuntu-latest
29
+ permissions:
30
+ pull-requests: write
31
+ contents: write
32
+ steps:
33
+ - uses: actions/checkout@v4
34
+ with:
35
+ ref: main
36
+ fetch-tags: true
37
+ - uses: actions/setup-node@v4.0.2
38
+ with:
39
+ node-version: 18
40
+ registry-url: https://registry.npmjs.org/
41
+ - uses: pnpm/action-setup@v3
42
+ - run: pnpm version patch --no-git-tag-version
43
+ - name: get-npm-version
44
+ id: version-after
45
+ uses: martinbeentjes/npm-get-version-action@v1.3.1
46
+ - name: Create Pull Request
47
+ uses: peter-evans/create-pull-request@v6
48
+ with:
49
+ add-paths: |
50
+ package.json
51
+ delete-branch: true
52
+ title: Bump to version ${{ steps.version-after.outputs.current-version }}
53
+ commit-message: |
54
+ Bump to version ${{ steps.version-after.outputs.current-version }}
55
+ token: ${{ secrets.PAT }}
56
+ committer: AutoHippo <auto@hippo.org>
57
+ branch: bump-to-version-${{ steps.version-after.outputs.current-version }}
package/README.md ADDED
@@ -0,0 +1,44 @@
1
+ ![node](https://img.shields.io/node/v/homebridge-eggtimer-plugin)
2
+ [![npm](https://img.shields.io/npm/dt/homebridge-eggtimer-plugin.svg)](https://www.npmjs.com/package/homebridge-eggtimer-plugin)
3
+ [![npm version](https://badge.fury.io/js/homebridge-eggtimer-plugin.svg)](https://badge.fury.io/js/homebridge-eggtimer-plugin)
4
+ ![Node.js CI](https://github.com/teh-hippo/homebridge-eggtimer-plugin/workflows/Node.js%20CI/badge.svg)
5
+
6
+ [![verified-by-homebridge](https://badgen.net/badge/homebridge/verified/purple)](https://github.com/homebridge/homebridge/wiki/Verified-Plugins)
7
+
8
+ # Homebridge Egg Timer Plugin
9
+
10
+ Example config.json:
11
+
12
+ ```json
13
+ "accessories": [
14
+ {
15
+ "name": "Timer Bulb 1",
16
+ "interval": 60000,
17
+ "stateful": false,
18
+ "occupancySensor": false,
19
+ "accessory": "EggTimerBulb",
20
+ },
21
+ ]
22
+ ```
23
+
24
+ This plugin will create a fake bulb that provides an everyday egg-timer for automations.
25
+ Brightness is used as an indicator of the time remaining and can be adjusted after the time has started.
26
+ When turned, the brightness will be decremented at set intervals (per minute by default).
27
+ HomeKit automations can be triggered based on the timer commencing (bulb on) and on completion (bulb off).
28
+
29
+ Unlike [homebridge-delay-switch](https://github.com/nitaybz/homebridge-delay-switch), the timer length is provided each time - similar to a real-world egg timer.
30
+
31
+ This was created originally created to help with kids' bedtime routines, where nightlights would be turned off automatically after an allowed amount of reading time (call me crazy, its 2023).
32
+
33
+ Other example usages found since:
34
+
35
+ * Automating a desk heater for bursts of heat in the winter.
36
+ * Running our air-con for a while and having it turn off automatically.
37
+
38
+ ## Parameters
39
+
40
+ | Parameter | Description | Default |
41
+ | --------- | ----- | ------- |
42
+ | `interval`| How often to decrement the brightness. | `60000` (1 minute) |
43
+ | `stateful`| Persist the timer state between restarts. | `false` |
44
+ | `occupancySensor`| Add an occupancy sensor that reflects the timer's current state. | `false` |
package/SECURITY.md ADDED
@@ -0,0 +1,11 @@
1
+ # Security Policy
2
+
3
+ ## Supported Versions
4
+
5
+ | Version | Supported |
6
+ | ------- | ------------------ |
7
+ | 1.0.x | :white_check_mark: |
8
+
9
+ ## Reporting a Vulnerability
10
+
11
+ Create an issue and it'll get sorted.
@@ -0,0 +1,37 @@
1
+ {
2
+ "pluginAlias": "EggTimerBulb",
3
+ "pluginType": "accessory",
4
+ "singular": false,
5
+ "headerDisplay": "Everyday egg timers for HomeBridge",
6
+ "footerDisplay": "Created by teh-hippo",
7
+ "schema": {
8
+ "type": "object",
9
+ "properties": {
10
+ "name": {
11
+ "title": "Accessory Name",
12
+ "description": "Name for the accessory",
13
+ "type": "string",
14
+ "required": true
15
+ },
16
+ "interval": {
17
+ "title": "Interval Time in Milliseconds",
18
+ "description": "Amount of time in milliseconds to wait in between each decrement of the brightness",
19
+ "type": "integer",
20
+ "default": 60000,
21
+ "required": true
22
+ },
23
+ "stateful": {
24
+ "title": "Stateful",
25
+ "description": "Persist the timer state between restarts.",
26
+ "type": "boolean",
27
+ "required": false
28
+ },
29
+ "occupancySensor": {
30
+ "title": "Occupancy Sensor",
31
+ "description": "Add an occupancy sensor that reflects the timer's current state",
32
+ "type": "boolean",
33
+ "required": false
34
+ }
35
+ }
36
+ }
37
+ }
@@ -0,0 +1,4 @@
1
+ import { API } from "homebridge";
2
+ declare const _default: (api: API) => void;
3
+ export = _default;
4
+ //# sourceMappingURL=accessory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"accessory.d.ts","sourceRoot":"","sources":["../src/accessory.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,GAAG,EAKJ,MAAM,YAAY,CAAC;8BA2KL,GAAG;AAAlB,kBAEE"}
@@ -0,0 +1,134 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ const node_persist_1 = require("node-persist");
6
+ const async_lock_1 = __importDefault(require("async-lock"));
7
+ class EggTimerBulb {
8
+ constructor(log, config, api) {
9
+ this.brightness = 0;
10
+ this.log = log;
11
+ this.hap = api.hap;
12
+ this.interval = Number(config.interval);
13
+ this.stateRestored = false;
14
+ this.lightbulbService = new this.hap.Service.Lightbulb(config.name);
15
+ this.lightbulbService.getCharacteristic(this.hap.Characteristic.On)
16
+ .onGet(this.getOn.bind(this))
17
+ .onSet(this.setOn.bind(this));
18
+ this.lightbulbService.getCharacteristic(this.hap.Characteristic.Brightness)
19
+ .onGet(this.getBrightness.bind(this))
20
+ .onSet(this.setBrightness.bind(this));
21
+ this.informationService = new this.hap.Service.AccessoryInformation()
22
+ .setCharacteristic(this.hap.Characteristic.Manufacturer, "Egg Timer Bulb")
23
+ .setCharacteristic(this.hap.Characteristic.Model, `${config.name} (${this.interval.toString()}ms)`);
24
+ this.stateful = config.stateful === true;
25
+ this.storageKey = `${config.name}-${this.interval.toString()}`;
26
+ this.storageDir = api.user.persistPath();
27
+ this.lock = new async_lock_1.default();
28
+ if (config.occupancySensor === true) {
29
+ this.occupancyService = new this.hap.Service.OccupancySensor(`${config.name} Active`);
30
+ this.occupancyService.getCharacteristic(this.hap.Characteristic.OccupancyDetected)
31
+ .onGet(this.getOccupancy.bind(this));
32
+ }
33
+ if (this.stateful) {
34
+ this.restoreState().catch((error) => {
35
+ this.log.error(String(error));
36
+ });
37
+ }
38
+ }
39
+ getServices() {
40
+ const services = [
41
+ this.informationService,
42
+ this.lightbulbService
43
+ ];
44
+ if (this.occupancyService) {
45
+ services.push(this.occupancyService);
46
+ }
47
+ return services;
48
+ }
49
+ async getOn() {
50
+ await this.restoreState();
51
+ return this.brightness > 0;
52
+ }
53
+ async setOn(value) {
54
+ if (!value) {
55
+ this.log.info("Manually stopping timer.");
56
+ await this.updateBrightness(0);
57
+ }
58
+ }
59
+ async getBrightness() {
60
+ await this.restoreState();
61
+ return this.brightness;
62
+ }
63
+ async setBrightness(value) {
64
+ const brightness = value;
65
+ await this.updateBrightness(brightness);
66
+ }
67
+ async getOccupancy() {
68
+ await this.restoreState();
69
+ return this.brightness > 0;
70
+ }
71
+ async updateBrightness(value) {
72
+ this.log.debug(`Brightness: ${this.brightness.toString()} -> ${value.toString()}`);
73
+ this.brightness = Math.max(0, Math.min(100, value));
74
+ // Persist state
75
+ if (this.stateful) {
76
+ if (this.brightness > 0) {
77
+ this.log.debug("Caching state");
78
+ await (0, node_persist_1.set)(this.storageKey, this.brightness);
79
+ }
80
+ else {
81
+ this.log.debug("Deleting state");
82
+ await (0, node_persist_1.del)(this.storageKey);
83
+ }
84
+ }
85
+ // Update HomeKit
86
+ const isActive = this.brightness > 0;
87
+ this.lightbulbService.updateCharacteristic(this.hap.Characteristic.Brightness, this.brightness);
88
+ this.lightbulbService.updateCharacteristic(this.hap.Characteristic.On, isActive);
89
+ if (this.occupancyService !== undefined) {
90
+ this.occupancyService.updateCharacteristic(this.hap.Characteristic.OccupancyDetected, isActive);
91
+ }
92
+ // Update Timer
93
+ if (isActive && this.timer === undefined) {
94
+ this.log.info("Starting timer");
95
+ this.timer = setInterval(() => {
96
+ this.updateBrightness(this.brightness - 1).catch((error) => {
97
+ this.log.error(String(error));
98
+ });
99
+ }, this.interval);
100
+ }
101
+ else if (this.brightness === 0) {
102
+ this.log.info("Timer completed.");
103
+ clearInterval(this.timer);
104
+ this.timer = undefined;
105
+ }
106
+ }
107
+ async restoreState() {
108
+ if (!this.stateful || this.stateRestored) {
109
+ return;
110
+ }
111
+ await this.lock.acquire("restoreState", async () => {
112
+ // Double-lock
113
+ if (this.stateRestored) {
114
+ return;
115
+ }
116
+ this.log.debug("Checking for stored state.");
117
+ await (0, node_persist_1.init)({
118
+ expiredInterval: 1000 * 60 * 60 * 24 * 14, // Delete cached items after 14 days.
119
+ forgiveParseErrors: true,
120
+ dir: this.storageDir
121
+ });
122
+ const value = await (0, node_persist_1.get)(this.storageKey);
123
+ if (value !== undefined && value > 0) {
124
+ this.log.info(`Restoring state to: ${value.toString()}`);
125
+ await this.updateBrightness(value);
126
+ }
127
+ this.stateRestored = true;
128
+ });
129
+ }
130
+ }
131
+ module.exports = (api) => {
132
+ api.registerAccessory("EggTimerBulb", EggTimerBulb);
133
+ };
134
+ //# sourceMappingURL=accessory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"accessory.js","sourceRoot":"","sources":["../src/accessory.ts"],"names":[],"mappings":";;;;AAUA,+CAKsB;AAEtB,4DAAmC;AAEnC,MAAM,YAAY;IAehB,YAAY,GAAY,EAAE,MAAuB,EAAE,GAAQ;QAJnD,eAAU,GAAG,CAAC,CAAC;QAKrB,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC;QACf,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC,GAAG,CAAC;QACnB,IAAI,CAAC,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QACxC,IAAI,CAAC,aAAa,GAAG,KAAK,CAAC;QAE3B,IAAI,CAAC,gBAAgB,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QAEpE,IAAI,CAAC,gBAAgB,CAAC,iBAAiB,CAAC,IAAI,CAAC,GAAG,CAAC,cAAc,CAAC,EAAE,CAAC;aAChE,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;aAC5B,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;QAEhC,IAAI,CAAC,gBAAgB,CAAC,iBAAiB,CAAC,IAAI,CAAC,GAAG,CAAC,cAAc,CAAC,UAAU,CAAC;aACxE,KAAK,CAAC,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;aACpC,KAAK,CAAC,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;QAExC,IAAI,CAAC,kBAAkB,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,oBAAoB,EAAE;aAClE,iBAAiB,CAAC,IAAI,CAAC,GAAG,CAAC,cAAc,CAAC,YAAY,EAAE,gBAAgB,CAAC;aACzE,iBAAiB,CAAC,IAAI,CAAC,GAAG,CAAC,cAAc,CAAC,KAAK,EAAE,GAAG,MAAM,CAAC,IAAI,KAAK,IAAI,CAAC,QAAQ,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;QAEtG,IAAI,CAAC,QAAQ,GAAG,MAAM,CAAC,QAAQ,KAAK,IAAI,CAAC;QACzC,IAAI,CAAC,UAAU,GAAG,GAAG,MAAM,CAAC,IAAI,IAAI,IAAI,CAAC,QAAQ,CAAC,QAAQ,EAAE,EAAE,CAAC;QAC/D,IAAI,CAAC,UAAU,GAAG,GAAG,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC;QACzC,IAAI,CAAC,IAAI,GAAG,IAAI,oBAAS,EAAE,CAAC;QAE5B,IAAI,MAAM,CAAC,eAAe,KAAK,IAAI,EAAE,CAAC;YACpC,IAAI,CAAC,gBAAgB,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,eAAe,CAAC,GAAG,MAAM,CAAC,IAAI,SAAS,CAAC,CAAC;YACtF,IAAI,CAAC,gBAAgB,CAAC,iBAAiB,CAAC,IAAI,CAAC,GAAG,CAAC,cAAc,CAAC,iBAAiB,CAAC;iBAC/E,KAAK,CAAC,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;QACzC,CAAC;QAED,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAClB,IAAI,CAAC,YAAY,EAAE,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;gBAC3C,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;YAChC,CAAC,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,WAAW;QACT,MAAM,QAAQ,GAAG;YACf,IAAI,CAAC,kBAAkB;YACvB,IAAI,CAAC,gBAAgB;SACtB,CAAC;QAEF,IAAI,IAAI,CAAC,gBAAgB,EAAE,CAAC;YAC1B,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC;QACvC,CAAC;QAED,OAAO,QAAQ,CAAC;IAClB,CAAC;IAEO,KAAK,CAAC,KAAK;QACjB,MAAM,IAAI,CAAC,YAAY,EAAE,CAAC;QAC1B,OAAO,IAAI,CAAC,UAAU,GAAG,CAAC,CAAC;IAC7B,CAAC;IAEO,KAAK,CAAC,KAAK,CAAC,KAA0B;QAC5C,IAAI,CAAE,KAAiB,EAAE,CAAC;YACxB,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,0BAA0B,CAAC,CAAC;YAC1C,MAAM,IAAI,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC;QACjC,CAAC;IACH,CAAC;IAEO,KAAK,CAAC,aAAa;QACzB,MAAM,IAAI,CAAC,YAAY,EAAE,CAAC;QAC1B,OAAO,IAAI,CAAC,UAAU,CAAC;IACzB,CAAC;IAEO,KAAK,CAAC,aAAa,CAAC,KAA0B;QACpD,MAAM,UAAU,GAAG,KAAe,CAAC;QACnC,MAAM,IAAI,CAAC,gBAAgB,CAAC,UAAU,CAAC,CAAC;IAC1C,CAAC;IAEO,KAAK,CAAC,YAAY;QACxB,MAAM,IAAI,CAAC,YAAY,EAAE,CAAC;QAC1B,OAAO,IAAI,CAAC,UAAU,GAAG,CAAC,CAAC;IAC7B,CAAC;IAEO,KAAK,CAAC,gBAAgB,CAAC,KAAa;QAC1C,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,eAAe,IAAI,CAAC,UAAU,CAAC,QAAQ,EAAE,OAAO,KAAK,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;QACnF,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC;QAEpD,gBAAgB;QAChB,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAClB,IAAI,IAAI,CAAC,UAAU,GAAG,CAAC,EAAE,CAAC;gBACxB,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,eAAe,CAAC,CAAC;gBAChC,MAAM,IAAA,kBAAG,EAAC,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;YAC9C,CAAC;iBAAM,CAAC;gBACN,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,gBAAgB,CAAC,CAAC;gBACjC,MAAM,IAAA,kBAAG,EAAC,IAAI,CAAC,UAAU,CAAC,CAAC;YAC7B,CAAC;QACH,CAAC;QAED,iBAAiB;QACjB,MAAM,QAAQ,GAAG,IAAI,CAAC,UAAU,GAAG,CAAC,CAAC;QACrC,IAAI,CAAC,gBAAgB,CAAC,oBAAoB,CAAC,IAAI,CAAC,GAAG,CAAC,cAAc,CAAC,UAAU,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;QAChG,IAAI,CAAC,gBAAgB,CAAC,oBAAoB,CAAC,IAAI,CAAC,GAAG,CAAC,cAAc,CAAC,EAAE,EAAE,QAAQ,CAAC,CAAC;QACjF,IAAI,IAAI,CAAC,gBAAgB,KAAK,SAAS,EAAE,CAAC;YACxC,IAAI,CAAC,gBAAgB,CAAC,oBAAoB,CAAC,IAAI,CAAC,GAAG,CAAC,cAAc,CAAC,iBAAiB,EAAE,QAAQ,CAAC,CAAC;QAClG,CAAC;QAED,eAAe;QACf,IAAI,QAAQ,IAAI,IAAI,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;YACzC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC;YAChC,IAAI,CAAC,KAAK,GAAG,WAAW,CAAC,GAAG,EAAE;gBAC5B,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,UAAU,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;oBAClE,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;gBAChC,CAAC,CAAC,CAAC;YACL,CAAC,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;QACpB,CAAC;aAAM,IAAI,IAAI,CAAC,UAAU,KAAK,CAAC,EAAE,CAAC;YACjC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAC;YAClC,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YAC1B,IAAI,CAAC,KAAK,GAAG,SAAS,CAAC;QACzB,CAAC;IACH,CAAC;IAEO,KAAK,CAAC,YAAY;QACxB,IAAI,CAAC,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,aAAa,EAAE,CAAC;YACzC,OAAO;QACT,CAAC;QAED,MAAM,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,cAAc,EAAE,KAAK,IAAI,EAAE;YACjD,cAAc;YACd,IAAI,IAAI,CAAC,aAAa,EAAE,CAAC;gBACvB,OAAO;YACT,CAAC;YAED,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,4BAA4B,CAAC,CAAC;YAC7C,MAAM,IAAA,mBAAI,EAAC;gBACT,eAAe,EAAE,IAAI,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE,qCAAqC;gBAChF,kBAAkB,EAAE,IAAI;gBACxB,GAAG,EAAE,IAAI,CAAC,UAAU;aACrB,CAAC,CAAC;YAEH,MAAM,KAAK,GAAG,MAAM,IAAA,kBAAG,EAAC,IAAI,CAAC,UAAU,CAAuB,CAAC;YAC/D,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;gBACrC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,uBAAuB,KAAK,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;gBACzD,MAAM,IAAI,CAAC,gBAAgB,CAAC,KAAK,CAAC,CAAC;YACrC,CAAC;YAED,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC;QAC5B,CAAC,CAAC,CAAC;IACL,CAAC;CACF;AAED,iBAAS,CAAC,GAAQ,EAAE,EAAE;IACpB,GAAG,CAAC,iBAAiB,CAAC,cAAc,EAAE,YAAY,CAAC,CAAC;AACtD,CAAC,CAAC"}
@@ -0,0 +1,36 @@
1
+ /* eslint-disable @typescript-eslint/no-unsafe-argument */
2
+ // @ts-check
3
+
4
+ //import eslint from "@eslint/js";
5
+ const eslint = require('@eslint/js')
6
+ //import tseslint from "typescript-eslint";
7
+ const tseslint = require('typescript-eslint')
8
+ //import stylistic from "@stylistic/eslint-plugin";
9
+ const stylistic = require('@stylistic/eslint-plugin')
10
+
11
+ module.exports = [
12
+ ...tseslint.config(
13
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
14
+ eslint.configs.recommended,
15
+ ...tseslint.configs.stylisticTypeChecked,
16
+ ...tseslint.configs.strictTypeChecked,
17
+ {
18
+ languageOptions: {
19
+ parserOptions: {
20
+ project: true
21
+ }
22
+ }
23
+ }
24
+ ),
25
+ stylistic.configs.customize({
26
+ indent: 2,
27
+ quotes: "double",
28
+ commaDangle: "never",
29
+ quoteProps: "as-needed",
30
+ arrowParens: false,
31
+ blockSpacing: true,
32
+ braceStyle: "1tbs",
33
+ flat: true,
34
+ semi: true
35
+ })
36
+ ];
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "homebridge-eggtimer-plugin",
3
+ "displayName": "Homebridge Eggtimer Plugin",
4
+ "version": "1.0.30",
5
+ "description": "Egg Timers for Homebridge: https://github.com/nfarina/homebridge",
6
+ "license": "MIT",
7
+ "keywords": [
8
+ "homebridge-plugin",
9
+ "egg-timer",
10
+ "eggtimer",
11
+ "timer",
12
+ "countdown",
13
+ "delay",
14
+ "automation",
15
+ "homebridge",
16
+ "persistent"
17
+ ],
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git://github.com/teh-hippo/homebridge-eggtimer-plugin.git"
21
+ },
22
+ "bugs": {
23
+ "url": "http://github.com/teh-hippo/homebridge-eggtimer-plugin/issues"
24
+ },
25
+ "funding": {
26
+ "type": "github",
27
+ "url": "https://github.com/sponsors/teh-hippo"
28
+ },
29
+ "main": "dist/accessory.js",
30
+ "engines": {
31
+ "node": ">=16.0.0",
32
+ "homebridge": ">=1.3.5"
33
+ },
34
+ "devDependencies": {
35
+ "@stylistic/eslint-plugin": "^1.8.0",
36
+ "@types/async-lock": "^1.4.2",
37
+ "@types/node": "^20.12.8",
38
+ "@types/node-persist": "^3.1.8",
39
+ "eslint": "^8.57.0",
40
+ "homebridge": "^1.8.1",
41
+ "rimraf": "^5.0.5",
42
+ "ts-node": "^10.9.2",
43
+ "typescript": "^5.4.5",
44
+ "typescript-eslint": "^7.8.0"
45
+ },
46
+ "dependencies": {
47
+ "async-lock": "^1.4.1",
48
+ "node-persist": "^4.0.1"
49
+ },
50
+ "packageManager": "pnpm@8.15.8+sha256.691fe176eea9a8a80df20e4976f3dfb44a04841ceb885638fe2a26174f81e65e",
51
+ "scripts": {
52
+ "lint": "eslint src",
53
+ "lintAndFix": "eslint --fix src",
54
+ "debug": "tsc && homebridge -I -D",
55
+ "build": "rimraf ./dist && tsc"
56
+ }
57
+ }
@@ -0,0 +1,182 @@
1
+ import {
2
+ AccessoryConfig,
3
+ AccessoryPlugin,
4
+ API,
5
+ CharacteristicValue,
6
+ HAP,
7
+ Logging,
8
+ Service
9
+ } from "homebridge";
10
+
11
+ import {
12
+ init,
13
+ set,
14
+ get,
15
+ del
16
+ } from "node-persist";
17
+
18
+ import AsyncLock from "async-lock";
19
+
20
+ class EggTimerBulb implements AccessoryPlugin {
21
+ private readonly log: Logging;
22
+ private readonly lightbulbService: Service;
23
+ private readonly informationService: Service;
24
+ private readonly occupancyService: Service | undefined;
25
+ private readonly interval: number;
26
+ private readonly hap: HAP;
27
+ private readonly storageKey: string;
28
+ private readonly storageDir: string;
29
+ private readonly stateful: boolean;
30
+ private readonly lock: AsyncLock;
31
+ private brightness = 0;
32
+ private timer: NodeJS.Timeout | undefined;
33
+ private stateRestored: boolean;
34
+
35
+ constructor(log: Logging, config: AccessoryConfig, api: API) {
36
+ this.log = log;
37
+ this.hap = api.hap;
38
+ this.interval = Number(config.interval);
39
+ this.stateRestored = false;
40
+
41
+ this.lightbulbService = new this.hap.Service.Lightbulb(config.name);
42
+
43
+ this.lightbulbService.getCharacteristic(this.hap.Characteristic.On)
44
+ .onGet(this.getOn.bind(this))
45
+ .onSet(this.setOn.bind(this));
46
+
47
+ this.lightbulbService.getCharacteristic(this.hap.Characteristic.Brightness)
48
+ .onGet(this.getBrightness.bind(this))
49
+ .onSet(this.setBrightness.bind(this));
50
+
51
+ this.informationService = new this.hap.Service.AccessoryInformation()
52
+ .setCharacteristic(this.hap.Characteristic.Manufacturer, "Egg Timer Bulb")
53
+ .setCharacteristic(this.hap.Characteristic.Model, `${config.name} (${this.interval.toString()}ms)`);
54
+
55
+ this.stateful = config.stateful === true;
56
+ this.storageKey = `${config.name}-${this.interval.toString()}`;
57
+ this.storageDir = api.user.persistPath();
58
+ this.lock = new AsyncLock();
59
+
60
+ if (config.occupancySensor === true) {
61
+ this.occupancyService = new this.hap.Service.OccupancySensor(`${config.name} Active`);
62
+ this.occupancyService.getCharacteristic(this.hap.Characteristic.OccupancyDetected)
63
+ .onGet(this.getOccupancy.bind(this));
64
+ }
65
+
66
+ if (this.stateful) {
67
+ this.restoreState().catch((error: unknown) => {
68
+ this.log.error(String(error));
69
+ });
70
+ }
71
+ }
72
+
73
+ getServices(): Service[] {
74
+ const services = [
75
+ this.informationService,
76
+ this.lightbulbService
77
+ ];
78
+
79
+ if (this.occupancyService) {
80
+ services.push(this.occupancyService);
81
+ }
82
+
83
+ return services;
84
+ }
85
+
86
+ private async getOn(): Promise<boolean> {
87
+ await this.restoreState();
88
+ return this.brightness > 0;
89
+ }
90
+
91
+ private async setOn(value: CharacteristicValue): Promise<void> {
92
+ if (!(value as boolean)) {
93
+ this.log.info("Manually stopping timer.");
94
+ await this.updateBrightness(0);
95
+ }
96
+ }
97
+
98
+ private async getBrightness(): Promise<number> {
99
+ await this.restoreState();
100
+ return this.brightness;
101
+ }
102
+
103
+ private async setBrightness(value: CharacteristicValue): Promise<void> {
104
+ const brightness = value as number;
105
+ await this.updateBrightness(brightness);
106
+ }
107
+
108
+ private async getOccupancy(): Promise<boolean> {
109
+ await this.restoreState();
110
+ return this.brightness > 0;
111
+ }
112
+
113
+ private async updateBrightness(value: number): Promise<void> {
114
+ this.log.debug(`Brightness: ${this.brightness.toString()} -> ${value.toString()}`);
115
+ this.brightness = Math.max(0, Math.min(100, value));
116
+
117
+ // Persist state
118
+ if (this.stateful) {
119
+ if (this.brightness > 0) {
120
+ this.log.debug("Caching state");
121
+ await set(this.storageKey, this.brightness);
122
+ } else {
123
+ this.log.debug("Deleting state");
124
+ await del(this.storageKey);
125
+ }
126
+ }
127
+
128
+ // Update HomeKit
129
+ const isActive = this.brightness > 0;
130
+ this.lightbulbService.updateCharacteristic(this.hap.Characteristic.Brightness, this.brightness);
131
+ this.lightbulbService.updateCharacteristic(this.hap.Characteristic.On, isActive);
132
+ if (this.occupancyService !== undefined) {
133
+ this.occupancyService.updateCharacteristic(this.hap.Characteristic.OccupancyDetected, isActive);
134
+ }
135
+
136
+ // Update Timer
137
+ if (isActive && this.timer === undefined) {
138
+ this.log.info("Starting timer");
139
+ this.timer = setInterval(() => {
140
+ this.updateBrightness(this.brightness - 1).catch((error: unknown) => {
141
+ this.log.error(String(error));
142
+ });
143
+ }, this.interval);
144
+ } else if (this.brightness === 0) {
145
+ this.log.info("Timer completed.");
146
+ clearInterval(this.timer);
147
+ this.timer = undefined;
148
+ }
149
+ }
150
+
151
+ private async restoreState(): Promise<void> {
152
+ if (!this.stateful || this.stateRestored) {
153
+ return;
154
+ }
155
+
156
+ await this.lock.acquire("restoreState", async () => {
157
+ // Double-lock
158
+ if (this.stateRestored) {
159
+ return;
160
+ }
161
+
162
+ this.log.debug("Checking for stored state.");
163
+ await init({
164
+ expiredInterval: 1000 * 60 * 60 * 24 * 14, // Delete cached items after 14 days.
165
+ forgiveParseErrors: true,
166
+ dir: this.storageDir
167
+ });
168
+
169
+ const value = await get(this.storageKey) as number | undefined;
170
+ if (value !== undefined && value > 0) {
171
+ this.log.info(`Restoring state to: ${value.toString()}`);
172
+ await this.updateBrightness(value);
173
+ }
174
+
175
+ this.stateRestored = true;
176
+ });
177
+ }
178
+ }
179
+
180
+ export = (api: API) => {
181
+ api.registerAccessory("EggTimerBulb", EggTimerBulb);
182
+ };
package/tsconfig.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2018", // ~node10
4
+ "module": "commonjs",
5
+ "lib": [
6
+ "es2015",
7
+ "es2016",
8
+ "es2017",
9
+ "es2018"
10
+ ],
11
+ "declaration": true,
12
+ "declarationMap": true,
13
+ "sourceMap": true,
14
+ "outDir": "./dist",
15
+ "rootDir": "./src",
16
+ "strict": true,
17
+ "esModuleInterop": true,
18
+ "noImplicitAny": false
19
+ },
20
+ "include": [
21
+ "src/"
22
+ ]
23
+ }