chronograph-npm 0.1.4 → 0.2.0

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,15 @@
1
+ # These are supported funding model platforms
2
+
3
+ github: XeroxQ
4
+ patreon: QlusterIT
5
+ open_collective: # Replace with a single Open Collective username
6
+ ko_fi: # Replace with a single Ko-fi username
7
+ tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8
+ community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9
+ liberapay: # Replace with a single Liberapay username
10
+ issuehunt: # Replace with a single IssueHunt username
11
+ lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
12
+ polar: # Replace with a single Polar username
13
+ buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
14
+ thanks_dev: # Replace with a single thanks.dev username
15
+ custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
@@ -0,0 +1,19 @@
1
+ name: Add issues/PRs to project
2
+
3
+ on:
4
+ issues:
5
+ types:
6
+ - opened
7
+ pull_request:
8
+ types:
9
+ - opened
10
+
11
+ jobs:
12
+ add-to-project:
13
+ name: Add issue/PR to project
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - uses: actions/add-to-project@v2
17
+ with:
18
+ project-url: https://github.com/orgs/Qluster-IT/projects/1
19
+ github-token: ${{ secrets.PAT }}
@@ -0,0 +1,22 @@
1
+ name: Issue/PR assignment
2
+
3
+ on:
4
+ issues:
5
+ types: [opened]
6
+ pull_request:
7
+ types: [opened] #edited, synchronize, reopened
8
+
9
+ jobs:
10
+ auto-assign:
11
+ runs-on: ubuntu-latest
12
+ permissions:
13
+ issues: write
14
+ pull-requests: write
15
+ steps:
16
+ - name: 'Auto-assign issue/PR'
17
+ uses: pozil/auto-assign-issue@v4
18
+ with:
19
+ assignees: XeroxQ
20
+ numOfAssignee: 1
21
+ allowSelfAssign: true
22
+ repo-token: ${{ secrets.PAT }}
@@ -0,0 +1,82 @@
1
+ name: Project Item Unarchiveren en Status naar Ready
2
+
3
+ on:
4
+ issues:
5
+ types: [reopened]
6
+ pull_request:
7
+ types: [reopened]
8
+
9
+ jobs:
10
+ unarchive-and-move:
11
+ runs-on: ubuntu-latest
12
+ env:
13
+ # Hier splitsen we de URL op om de CLI-fout te voorkomen
14
+ PROJECT_OWNER: "Qluster-IT"
15
+ PROJECT_NUMBER: 1
16
+ GITHUB_TOKEN: ${{ secrets.PAT }}
17
+ ISSUE_NODE_ID: ${{ github.event.issue.node_id }}
18
+
19
+ steps:
20
+ - name: Zoek gekoppeld Project en Item via GraphQL
21
+ id: item_info
22
+ run: |
23
+ RESPONSE=$(gh api graphql -f query='
24
+ query($issueId: ID!) {
25
+ node(id: $issueId) {
26
+ ... on Issue {
27
+ projectItems(first: 10) {
28
+ nodes {
29
+ id
30
+ isArchived
31
+ project {
32
+ id
33
+ number
34
+ owner {
35
+ ... on Organization { login }
36
+ ... on User { login }
37
+ }
38
+ }
39
+ }
40
+ }
41
+ }
42
+ }
43
+ }
44
+ ' -f issueId="$ISSUE_NODE_ID")
45
+
46
+ # Filter de data voor het juiste project
47
+ ITEM_DATA=$(echo "$RESPONSE" | jq -r --argjson num "${{ env.PROJECT_NUMBER }}" --arg owner "${{ env.PROJECT_OWNER }}" \
48
+ '.data.node.projectItems.nodes[] | select(.project.number == $num and (.project.owner.login | ascii_downcase) == ($owner | ascii_downcase))')
49
+
50
+ ITEM_ID=$(echo "$ITEM_DATA" | jq -r '.id // empty')
51
+ PROJECT_ID=$(echo "$ITEM_DATA" | jq -r '.project.id // empty')
52
+ IS_ARCHIVED=$(echo "$ITEM_DATA" | jq -r '.isArchived // false')
53
+
54
+ echo "Gevonden Item ID: $ITEM_ID"
55
+ echo "Gevonden Project ID: $PROJECT_ID"
56
+ echo "Is gearchiveerd: $IS_ARCHIVED"
57
+
58
+ echo "item_id=$ITEM_ID" >> $GITHUB_OUTPUT
59
+ echo "project_id=$PROJECT_ID" >> $GITHUB_OUTPUT
60
+ echo "is_archived=$IS_ARCHIVED" >> $GITHUB_OUTPUT
61
+
62
+ - name: Unarchive het projectitem
63
+ if: steps.item_info.outputs.item_id != '' && steps.item_info.outputs.is_archived == 'true'
64
+ run: |
65
+ # We sturen nu verplicht projectId én itemId mee, en vragen 'item { id }' op als return
66
+ gh api graphql -f query='
67
+ mutation($projectId: ID!, $itemId: ID!) {
68
+ unarchiveProjectV2Item(input: {projectId: $projectId, itemId: $itemId}) {
69
+ item { id }
70
+ }
71
+ }
72
+ ' -f projectId="${{ steps.item_info.outputs.project_id }}" -f itemId="${{ steps.item_info.outputs.item_id }}"
73
+
74
+ - name: Zet status op Ready
75
+ if: steps.item_info.outputs.item_id != ''
76
+ run: |
77
+ # We gebruiken nu de URL van het issue in plaats van de --id vlag
78
+ gh project item-edit ${{ env.PROJECT_NUMBER }} \
79
+ --owner "${{ env.PROJECT_OWNER }}" \
80
+ --url "https://github.com/${{ env.PROJECT_OWNER }}/${{ github.event.repository.name }}/issues/${{ github.event.issue.number }}" \
81
+ --field "Status" \
82
+ --value "Ready"
package/README.md CHANGED
@@ -1 +1,96 @@
1
- ## NPM Package for the Chronograph App for Athom's Homey.
1
+ # Chronograph for Homey
2
+
3
+ `chronograph-npm` lets a Homey app access chronographs provided by the [Chronograph app](https://homey.app/a/nl.fellownet.chronograph/). It provides helpers for retrieving chronographs and receiving realtime updates.
4
+
5
+ ## Installation
6
+
7
+ Install the package in your Homey app project:
8
+
9
+ ```sh
10
+ npm install chronograph-npm
11
+ ```
12
+
13
+ Add Chronograph to the permissions in your app's `app.json`:
14
+
15
+ ```json
16
+ {
17
+ "permissions": [
18
+ "homey:app:nl.fellownet.chronograph"
19
+ ]
20
+ }
21
+ ```
22
+
23
+ ## Initialize
24
+
25
+ Import `CG` and initialize it from your app's `onInit` method:
26
+
27
+ ```js
28
+ const { CG } = require('chronograph-npm');
29
+
30
+ class MyApp extends Homey.App {
31
+ async onInit() {
32
+ this.chronograph = await CG.init({ homey: this.homey });
33
+ }
34
+ }
35
+
36
+ module.exports = MyApp;
37
+ ```
38
+
39
+ `CG.init` accepts one option, `homey`, which must be the Homey instance. It returns a `CG` instance and registers a realtime listener. It does not fetch chronographs during initialization; call `CG.getChronographs()` or `CG.getAllChronographs()` when you need that data. When Homey emits `unload` as the consuming app stops, `CG.destroy()` is called automatically to remove the listeners, without replacing your app's lifecycle methods. Cleanup cannot be guaranteed if the process is forcibly terminated.
40
+
41
+ ## Available API
42
+
43
+ The package exports the `CG` class:
44
+
45
+ | Method or property | Description |
46
+ | --- | --- |
47
+ | `CG.init({ homey })` | Initializes the integration and returns a `CG` instance. |
48
+ | `CG.getChronographs()` | Returns the current active chronographs as an array. Each snapshot contains `name`, `data`, `duration`, `targetDuration`, and `running`. |
49
+ | `CG.getAllChronographs()` | Returns configured chronograph names grouped by type, such as `timer`, `stopwatch`, and `transition`. |
50
+ | `CG.onChronographUpdate({ chronograph })` | Update hook. Override it to handle realtime updates; the default implementation does nothing. |
51
+ | `CG.destroy()` | Removes the realtime and unload listeners. Called automatically on Homey's `unload` event; can also be called manually. |
52
+ | `instance.ready` / `CG.ready` | Promise that resolves after successful initialization and realtime-listener registration. |
53
+ | `instance.isReady` / `CG.isReady` | Indicates whether the integration has become ready. |
54
+
55
+ For example, retrieve active chronographs after initialization:
56
+
57
+ ```js
58
+ const chronographs = await CG.getChronographs();
59
+ for (const chronograph of chronographs) {
60
+ this.log(chronograph.name, chronograph.running, chronograph.duration);
61
+ }
62
+ ```
63
+
64
+ Use `getAllChronographs()` when you need the configured names (including chronographs that are not currently active):
65
+
66
+ ```js
67
+ const configured = await CG.getAllChronographs();
68
+ this.log(configured.timer);
69
+ ```
70
+
71
+ ## Realtime updates
72
+
73
+ Override the update hook before initializing `CG`. The hook is called with `{ chronograph }` for each realtime update. If an update contains a `now` value, the package converts it to a JavaScript `Date`.
74
+
75
+ ```js
76
+ const { CG } = require('chronograph-npm');
77
+
78
+ class MyApp extends Homey.App {
79
+ async onInit() {
80
+ CG.onChronographUpdate = async ({ chronograph }) => {
81
+ this.log('Chronograph updated:', chronograph.name, chronograph.now);
82
+ };
83
+
84
+ this.chronograph = await CG.init({ homey: this.homey });
85
+ }
86
+ }
87
+
88
+ module.exports = MyApp;
89
+ ```
90
+
91
+ ## Requirements
92
+
93
+ - The Chronograph app must be installed on the same Homey.
94
+ - Your app must declare the Chronograph permission in `app.json`.
95
+ - Initialize the package with your app's Homey instance before calling the retrieval methods.
96
+ - Initialization only registers for realtime updates; fetch chronograph data explicitly using the retrieval methods.
package/dist/cg.d.ts CHANGED
@@ -2,14 +2,53 @@ export class CG {
2
2
  static init({ homey }?: {
3
3
  homey: any;
4
4
  }): Promise<CG>;
5
+ /**
6
+ * Removes the realtime and unload listeners. Called automatically when Homey emits unload.
7
+ */
5
8
  static destroy(): void;
6
9
  static getAllChronographs(): Promise<any>;
7
10
  static getChronographs(): Promise<any>;
8
11
  /**
9
- * @description Setable member, when set, will be activated on any variable update.
10
- * @param {Object} param0.variable
12
+ * Handles a realtime update from the Chronograph app. Override this method
13
+ * to receive updated chronograph data.
14
+ * @param {Object} param0 Update payload.
15
+ * @param {Object} param0.chronograph Realtime state snapshot, not a Chronograph class instance.
16
+ * @param {string} [param0.chronograph.id] Generated identifier; omitted from single-event updates.
17
+ * @param {string} param0.chronograph.name Configured chronograph name.
18
+ * @param {number} param0.chronograph.duration Elapsed duration in milliseconds, also exposed as elapsed.
19
+ * @param {number} param0.chronograph.elapsed Elapsed duration in milliseconds.
20
+ * @param {number|null} param0.chronograph.targetDuration Target duration in milliseconds.
21
+ * Stopwatches have no target (NaN before serialization, normally null over the API).
22
+ * @param {number|null} param0.chronograph.remaining Target duration minus elapsed duration, in milliseconds.
23
+ * Not meaningful for stopwatches; may be NaN or null after serialization.
24
+ * @param {boolean} param0.chronograph.running Whether the chronograph is running.
25
+ * @param {boolean} param0.chronograph.removed Whether this update reports removal of the chronograph.
26
+ * @param {Date} param0.chronograph.now Snapshot timestamp, converted to a Date by this package.
27
+ * @param {Object} param0.chronograph.data Chronograph metadata, excluding flowValues; may be incomplete during creation.
28
+ * @param {'Timer'|'Stopwatch'|'Transition'} [param0.chronograph.data.type] Chronograph kind.
29
+ * @param {number} [param0.chronograph.data.from] Transition starting value.
30
+ * @param {number} [param0.chronograph.data.to] Transition ending value.
31
+ * @param {number} [param0.chronograph.data.steps] Transition step count, limited by its duration.
11
32
  */
12
- static onChronographUpdate({ chronograph }: any): Promise<void>;
33
+ static onChronographUpdate({ chronograph }: {
34
+ chronograph: {
35
+ id?: string;
36
+ name: string;
37
+ duration: number;
38
+ elapsed: number;
39
+ targetDuration: number | null;
40
+ remaining: number | null;
41
+ running: boolean;
42
+ removed: boolean;
43
+ now: Date;
44
+ data: {
45
+ type?: 'Timer' | 'Stopwatch' | 'Transition';
46
+ from?: number;
47
+ to?: number;
48
+ steps?: number;
49
+ };
50
+ };
51
+ }): Promise<void>;
13
52
  constructor({ homey }: {
14
53
  homey: any;
15
54
  });
package/dist/cg.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cg.d.ts","sourceRoot":"","sources":["../src/cg.js"],"names":[],"mappings":"AAAA;IAGI;;oBAyBC;IA+BD,uBAQC;IAuCD,0CAEC;IAED,uCAEC;IAKD;;;OAGG;IACH,gEAGC;IA/FD;;OAWC;IATG,WAA6B;IAC7B,iBAAoB;IAIpB,WAAyC;IAM7C,2BAaC;IAVW,SAAqC;IAyBjD,uDAcC;IAGD,eAAe;IACf,kBAeC;CAyBJ"}
1
+ {"version":3,"file":"cg.d.ts","sourceRoot":"","sources":["../src/cg.js"],"names":[],"mappings":"AAAA;IAGI;;oBAWC;IA+BD;;OAEG;IACH,uBASC;IAuCD,0CAEC;IAED,uCAEC;IAKD;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH;QAlB0B,WAAW;YACE,EAAE,GAA9B,MAAM;YACqB,IAAI,EAA/B,MAAM;YACqB,QAAQ,EAAnC,MAAM;YACqB,OAAO,EAAlC,MAAM;YAC0B,cAAc,EAA9C,MAAM,GAAC,IAAI;YAEqB,SAAS,EAAzC,MAAM,GAAC,IAAI;YAEiB,OAAO,EAAnC,OAAO;YACqB,OAAO,EAAnC,OAAO;YACkB,GAAG,EAA5B,IAAI;YACuB,IAAI;gBAC4B,IAAI,GAA/D,OAAO,GAAC,WAAW,GAAC,YAAY;gBACC,IAAI,GAArC,MAAM;gBAC2B,EAAE,GAAnC,MAAM;gBAC2B,KAAK,GAAtC,MAAM;;;sBAKhB;IArHD;;OAWC;IATG,WAA6B;IAC7B,iBAAoB;IAIpB,WAAyC;IAM7C,2BAaC;IAVW,SAAqC;IA6BjD,uDAcC;IAGD,eAAe;IACf,kBAeC;CA2CJ"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chronograph-npm",
3
- "version": "0.1.4",
3
+ "version": "0.2.0",
4
4
  "description": "The Chronograph App for Athom's Homey",
5
5
  "main": "src/cg.js",
6
6
  "types": "./dist/cg.d.ts",
package/src/cg.js CHANGED
@@ -2,24 +2,10 @@ class CG {
2
2
 
3
3
 
4
4
  static async init({ homey } = {}) {
5
+ if (CG.Current) CG.destroy();
5
6
  let cg = new CG({ homey });
6
7
 
7
- if (!homey.app.__CG_OnUninit) {
8
- homey.app.__CG_OnUninit = homey.app.onUninit;
9
- homey.app.onUninit = async function (args) {
10
-
11
- try {
12
- if (CG) await CG.destroy();
13
- } catch (error) {
14
- console.log(error);
15
- }
16
- try {
17
- if (args) await CG.homey.__CG_OnUninit(...args); else await CG.homey.__CG_OnUninit();
18
- } catch (error) {
19
- let a = error;
20
- }
21
- };
22
- }
8
+ homey.once('unload', CG.destroy);
23
9
  try {
24
10
  await cg.init();
25
11
  } catch (error) {
@@ -45,7 +31,7 @@ class CG {
45
31
  try {
46
32
  this.api = await this.getAppApi(true);
47
33
  this.api.on('realtime', this.realtimeFunction);
48
- await this.api.get('/chronographs')
34
+ //await this.api.get('/chronographs')
49
35
  if (!CG.isReady && CG._defer) CG._defer.resolve();
50
36
  } catch (error) {
51
37
  this.homey.error('CG.init', error);
@@ -57,9 +43,13 @@ class CG {
57
43
 
58
44
 
59
45
 
46
+ /**
47
+ * Removes the realtime and unload listeners. Called automatically when Homey emits unload.
48
+ */
60
49
  static destroy() {
61
50
  try {
62
51
  if (!CG.Current) return;
52
+ CG.Current.homey.removeListener('unload', CG.destroy);
63
53
  if (CG.Current.api) {
64
54
  CG.Current.api.off('realtime', CG.Current.realtimeFunction);
65
55
  }
@@ -116,8 +106,26 @@ class CG {
116
106
 
117
107
 
118
108
  /**
119
- * @description Setable member, when set, will be activated on any variable update.
120
- * @param {Object} param0.variable
109
+ * Handles a realtime update from the Chronograph app. Override this method
110
+ * to receive updated chronograph data.
111
+ * @param {Object} param0 Update payload.
112
+ * @param {Object} param0.chronograph Realtime state snapshot, not a Chronograph class instance.
113
+ * @param {string} [param0.chronograph.id] Generated identifier; omitted from single-event updates.
114
+ * @param {string} param0.chronograph.name Configured chronograph name.
115
+ * @param {number} param0.chronograph.duration Elapsed duration in milliseconds, also exposed as elapsed.
116
+ * @param {number} param0.chronograph.elapsed Elapsed duration in milliseconds.
117
+ * @param {number|null} param0.chronograph.targetDuration Target duration in milliseconds.
118
+ * Stopwatches have no target (NaN before serialization, normally null over the API).
119
+ * @param {number|null} param0.chronograph.remaining Target duration minus elapsed duration, in milliseconds.
120
+ * Not meaningful for stopwatches; may be NaN or null after serialization.
121
+ * @param {boolean} param0.chronograph.running Whether the chronograph is running.
122
+ * @param {boolean} param0.chronograph.removed Whether this update reports removal of the chronograph.
123
+ * @param {Date} param0.chronograph.now Snapshot timestamp, converted to a Date by this package.
124
+ * @param {Object} param0.chronograph.data Chronograph metadata, excluding flowValues; may be incomplete during creation.
125
+ * @param {'Timer'|'Stopwatch'|'Transition'} [param0.chronograph.data.type] Chronograph kind.
126
+ * @param {number} [param0.chronograph.data.from] Transition starting value.
127
+ * @param {number} [param0.chronograph.data.to] Transition ending value.
128
+ * @param {number} [param0.chronograph.data.steps] Transition step count, limited by its duration.
121
129
  */
122
130
  static async onChronographUpdate({ chronograph }) {
123
131
  //BL.homey.log('onChronographUpdate: ', chronograph);
@@ -0,0 +1,50 @@
1
+ const assert = require('node:assert/strict');
2
+ const { EventEmitter } = require('node:events');
3
+ const { test } = require('node:test');
4
+ const { CG } = require('../src/cg');
5
+
6
+ function createHomey() {
7
+ const homey = new EventEmitter();
8
+ const api = new EventEmitter();
9
+ homey.api = { getApiApp: () => api };
10
+ homey.app = { onUninit() {} };
11
+ homey.error = (...args) => assert.fail(args.join(' '));
12
+ return { homey, api };
13
+ }
14
+
15
+ test('unload removes the realtime listener without replacing app lifecycle methods', async (t) => {
16
+ const { homey, api } = createHomey();
17
+ const onUninit = homey.app.onUninit;
18
+ t.after(() => CG.destroy());
19
+ await CG.init({ homey });
20
+
21
+ assert.equal(homey.app.onUninit, onUninit);
22
+ assert.equal(api.listenerCount('realtime'), 1);
23
+ assert.equal(homey.listenerCount('unload'), 1);
24
+
25
+ homey.emit('unload');
26
+
27
+ assert.equal(api.listenerCount('realtime'), 0);
28
+ assert.equal(homey.listenerCount('unload'), 0);
29
+ });
30
+
31
+ test('reinitialization and manual cleanup do not leave listeners behind', async (t) => {
32
+ const first = createHomey();
33
+ const second = createHomey();
34
+ t.after(() => CG.destroy());
35
+ await CG.init({ homey: first.homey });
36
+ await CG.init({ homey: second.homey });
37
+
38
+ assert.equal(first.api.listenerCount('realtime'), 0);
39
+ assert.equal(first.homey.listenerCount('unload'), 0);
40
+ assert.equal(second.api.listenerCount('realtime'), 1);
41
+ assert.equal(second.homey.listenerCount('unload'), 1);
42
+
43
+ first.homey.emit('unload');
44
+ assert.equal(second.api.listenerCount('realtime'), 1);
45
+
46
+ CG.destroy();
47
+ CG.destroy();
48
+ assert.equal(second.api.listenerCount('realtime'), 0);
49
+ assert.equal(second.homey.listenerCount('unload'), 0);
50
+ });