@rhizomatics/signalk-einklabel-plugin 0.8.0 → 0.8.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.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ # 0.8.1
2
+
3
+ - A preferred `provider` can now be set for `resources`.
4
+ - The example `tide.svg` sets this to `provider=tides` to ignore other providers, (such as `mareas-ihm`)
5
+ - Devices will repaint at startup even if no schedule slot has been missed prior to startup, if the template itself has subsequently changed
6
+
1
7
  # 0.8.0
2
8
 
3
9
  - New `settle` time, configurable and defaulting to 120 seconds, to wait after plugin startup before attempting to access paths
@@ -6,7 +12,6 @@
6
12
  - Path based subscriptions ignore temporarily missing paths, so displays don't waste their tiny batteries on flapping values
7
13
  - Documentation now has a FAQ plus lots of other ESL/eInk hacking links.
8
14
 
9
-
10
15
  # 0.7.1
11
16
 
12
17
  - Fix path value retrieved for Image Fields when running in live plugin
package/README.md CHANGED
@@ -13,11 +13,11 @@ A SignalK plugin to display data from SignalK paths, APIs and plugins on Electro
13
13
 
14
14
  ## What is an ESL?
15
15
 
16
- Electronic Shelf Labels are [eInk](https://en.wikipedia.org/wiki/E_Ink) devices that consume very little battery energy, presuming they are not constantly updated - the battery is used only when the display changes, and a periodic BLE check for incoming changes. Perfect for info that changes only once or twice a day, like tidal information.
16
+ Electronic Shelf Labels are [eInk](https://en.wikipedia.org/wiki/E_Ink) devices that consume very little battery energy, presuming they are not constantly updated - the battery is used only when the display changes (which can take 5-10 seconds) and a periodic BLE check for incoming changes. Perfect for info that changes only once or twice a day, like tidal information.
17
17
 
18
18
  Since they are designed to be used in large quantity in small shops, they are cheap and simple devices. Earlier models required dedicated controllers, or updates over Wifi or NFC, whereas many modern ones are standalone BLE devices that can be updated from a phone or server.
19
19
 
20
- Being battery operated, they can be stuck on anywhere without wiring - the only constraints are bluetooth range, visibility (they need ambient light since the display is more like paper than a traditional lit-up electronic display) and out of the weather since the devices are intended for indoor use.
20
+ Being battery operated, they can be stuck on anywhere without wiring - the only location constraints are bluetooth range, visibility (they need ambient light since the display is more like paper than a traditional lit-up electronic display) and out of the weather since the devices are intended for indoor use.
21
21
 
22
22
  ## Pre-requisites
23
23
 
@@ -72,7 +72,7 @@ npm install @rhizomatics/signalk-einklabel-plugin
72
72
 
73
73
  ![Tide Clock](docs/assets/screenshots/example_tidal_clock.png)
74
74
 
75
- The tide clock needs the [signalk-tides](https://github.com/openwatersio/signalk-tides) plugin to be installed and publishing tides to the Resources API. The [tide.svg](https://github.com/rhizomatics/signalk-einklabel-plugin/blob/main/templates/tide.svg) can be customized to run with other APIs or take data only from SignalK data paths.
75
+ The tide clock needs the [signalk-tides](https://github.com/openwatersio/signalk-tides) plugin to be installed and publishing tides to the Resources API. The [tide.svg](https://github.com/rhizomatics/signalk-einklabel-plugin/blob/main/templates/tide.svg) can be customized to run with other APIs or take data only from SignalK data paths. For example, `source=resources,resource=tides,provider=tides,path=extremes[0].time,format=local_time` gets the first tide time, ensures its the preferred `signalk-tides` provider and makes it a simple local time rather than a UTC date time.
76
76
 
77
77
  To show the lunar phase, the `environment.moon.phaseName` path is required, which can
78
78
  be easily achieved by installing and configuring the `derived-data` plugin.
@@ -117,7 +117,9 @@ For example, `path=environment.forecast.description` uses the default data sourc
117
117
 
118
118
  #### SignalK REST APIs
119
119
 
120
- The source can be overridden to use the SignalK server's Resources API instead. For example, `source=resources,resource=tides,path=station.name` picks the `tides` resource and pulls the `station.name` path out of the JSON response - this works for any resource type (`tides`, `waypoints`, `routes`, ...), and needs nothing configured: the plugin reaches the Resources API directly. Where a resource is specified, it will be fetched once for that render, and subsequent fields sourced from the same resource use that cached response.
120
+ The source can be overridden to use the SignalK server's Resources API instead. Change `source` to `resources` and specify which resource with `resource`. If there are multiple providers for the same resource, and they're not equally useful, then either set a default provider in SignalK, or use the `provider` tag to set the name.
121
+
122
+ For example, `source=resources,resource=tides,provider=tides,path=station.name` picks the `tides` resource and pulls the `station.name` path out of the JSON response - this works for any resource type (`tides`, `waypoints`, `routes`, ...), and needs nothing configured: the plugin reaches the Resources API directly. Where a resource is specified, it will be fetched once for that render, and subsequent fields sourced from the same resource use that cached response. `provider` is optional, the default provider will be used if not specified.
121
123
 
122
124
  #### Plugin Data
123
125
 
@@ -150,7 +152,7 @@ Common categories:
150
152
 
151
153
  Additionally, `round=n` can be used to round to limited decimal places.
152
154
 
153
- These can all be combined as in `source=resources,resource=tides,path=extremes[2].level,category=depth,round=2`
155
+ These can all be combined as in `source=resources,resource=tides,provider=tides,path=extremes[2].level,category=depth,round=2`
154
156
 
155
157
  ### Non-Textual Fields (Images)
156
158
 
@@ -174,7 +176,6 @@ Three font types are loaded by default, use the generic font family, or exact fo
174
176
  - `sans-serif` - `Roboto`
175
177
  - `monospace` - `Roboto Mono`
176
178
 
177
-
178
179
  ## Vendors
179
180
 
180
181
  ### Zhsunyco
@@ -253,7 +254,6 @@ Inkscape adds its own metadata to images, which can be stripped off by exporting
253
254
 
254
255
  Due to a limitation in the `resvg-wasm` library used to turn SVGs into images, the `font-family` is limited to `serif`,`sans-serif`,`monospace` or the exact name of one of the installed fonts - `Roboto` (sans serif), `Roboto Serif` or `Roboto Mono`. Inkscape has its own fonts, which won't match what's available in the SignalK plugin, so for more precise design, install [Roboto from Google](https://fonts.google.com/specimen/Roboto) via the web page, `brew` on MacOS or similar.
255
256
 
256
-
257
257
  ### Debugging Templates
258
258
 
259
259
  The `esl-cli` can be used to debug and validate templates quickly:
@@ -272,25 +272,25 @@ npx esl-cli fields -t templates/tide.svg -u http://localhost
272
272
  ```
273
273
 
274
274
  ```
275
- id spec value
276
- station.name source=resources,resource=tides,path=station.name Tobermory
277
- source.name. source=resources,resource=tides,path=station.source.name TICON-4
278
- last_repaint source=einklabel,path=repainted,format=local_datetime_short 30 Jun 26 00:08
279
- extremes.0 source=resources,resource=tides,path=extremes[0].label Low
280
- extremes.1 source=resources,resource=tides,path=extremes[1].label High
281
- extremes.2 source=resources,resource=tides,path=extremes[2].label Low
282
- timezoneRegion source=einklabel,path=local_zone BST
283
- lat source=resources,resource=tides,path=station.datums.LAT,category=depth 0.2m
284
- hat source=resources,resource=tides,path=station.datums.HAT,category=depth 5.2m
285
- extremes.2.level source=resources,resource=tides,path=extremes[2].level,category=depth 1.1m
286
- extremes.2.time source=resources,resource=tides,path=extremes[2].time,format=local_time 13:15
287
- extremes.1.level source=resources,resource=tides,path=extremes[1].level,category=depth 3.8m
288
- extremes.1.time source=resources,resource=tides,path=extremes[1].time,format=local_time 07:05
289
- extremes.0.time source=resources,resource=tides,path=extremes[0].time,format=local_time 01:21
290
- extremes.0.time-8 source=resources,resource=tides,path=extremes[0].time,format=day_mon 30 Jun
291
- extremes.0.time-8-5 source=resources,resource=tides,path=extremes[1].time,format=day_mon 30 Jun
292
- extremes.0.time-8-9 source=resources,resource=tides,path=extremes[2].time,format=day_mon 30 Jun
293
- extremes.0.level source=resources,resource=tides,path=extremes[0].level,category=depth 1.3m
275
+ id spec value
276
+ station.name source=resources,resource=tides,provider=tides,path=station.name Tobermory
277
+ source.name. source=resources,resource=tides,provider=tides,path=station.source.name TICON-4
278
+ last_repaint source=einklabel,path=repainted,format=local_datetime_short 30 Jun 26 00:08
279
+ extremes.0 source=resources,resource=tides,provider=tides,path=extremes[0].label Low
280
+ extremes.1 source=resources,resource=tides,provider=tides,path=extremes[1].label High
281
+ extremes.2 source=resources,resource=tides,provider=tides,path=extremes[2].label Low
282
+ timezoneRegion source=einklabel,path=local_zone BST
283
+ lat source=resources,resource=tides,provider=tides,path=station.datums.LAT,category=depth 0.2m
284
+ hat source=resources,resource=tides,provider=tides,path=station.datums.HAT,category=depth 5.2m
285
+ extremes.2.level source=resources,resource=tides,provider=tides,path=extremes[2].level,category=depth 1.1m
286
+ extremes.2.time source=resources,resource=tides,provider=tides,path=extremes[2].time,format=local_time 13:15
287
+ extremes.1.level source=resources,resource=tides,provider=tides,path=extremes[1].level,category=depth 3.8m
288
+ extremes.1.time source=resources,resource=tides,provider=tides,path=extremes[1].time,format=local_time 07:05
289
+ extremes.0.time source=resources,resource=tides,provider=tides,path=extremes[0].time,format=local_time 01:21
290
+ extremes.0.time-8 source=resources,resource=tides,provider=tides,path=extremes[0].time,format=day_mon 30 Jun
291
+ extremes.0.time-8-5 source=resources,resource=tides,provider=tides,path=extremes[1].time,format=day_mon 30 Jun
292
+ extremes.0.time-8-9 source=resources,resource=tides,provider=tides,path=extremes[2].time,format=day_mon 30 Jun
293
+ extremes.0.level source=resources,resource=tides,provider=tides,path=extremes[0].level,category=depth 1.3m
294
294
  ```
295
295
 
296
296
  ### Offline Working
@@ -309,7 +309,7 @@ For example, `npx esl-cli fields -t templates/tide.svg -e examples` will show al
309
309
 
310
310
  If the plugin repaints a display at server startup, then the plugin that provides the data may not have started ( or in the case of `derived-data` the plugin that the plugin depends on! ) and unlike Home Assistant, there's no good way of sequencing the start of plugins.
311
311
 
312
- Use the *settle* time, to impose a minimum wait between the eInk Label plugin being initialized, and it attempting to paint any displays, and increase this value if its still missing data.
312
+ Use the _settle_ time, to impose a minimum wait between the eInk Label plugin being initialized, and it attempting to paint any displays, and increase this value if its still missing data.
313
313
 
314
314
  ### Times are showing incorrectly {#faq-timezone}
315
315
 
@@ -329,7 +329,7 @@ Try a BLE proxy device, ESP32 is popular for this.
329
329
  - [eInk Dashboard Modern SK](https://github.com/VladimirKalachikhin/e-inkDashboardModernSK) - SignalK dashboard for non-ESL eInk display.
330
330
  - [esp32-esl-system](https://github.com/giobauermeister/esp32-esl-system) - Docker and ESP32 based system for updating ESLs.
331
331
  - [hass-gicisky](https://github.com/eigger/hass-gicisky) - Home Assistant integration for Gicisky ESLs ( a similar vendor to Zhsunyco). Uses [imagespec](https://github.com/eigger/imagespec) for templating.
332
- - [ha-panda](https://github.com/moryoav/ha-panda) - Home Assistant integration for Panda ESLs ( a similar vendor to Zhsunyco).
332
+ - [ha-panda](https://github.com/moryoav/ha-panda) - Home Assistant integration for Panda ESLs ( a similar vendor to Zhsunyco).
333
333
  - [Dmitry.gr](https://dmitry.gr/?r=05.Projects&proj=29.%20eInk%20Price%20Tags) - Personal site of an ESL hacker
334
334
  - [Aaron Christobel](https://www.youtube.com/@atc1441) - YouTube channel of an ESL hacker.
335
335
  - [rbaron.net](https://rbaron.net/blog/2022/07/29/Daisy-chaining-multiple-electronic-shelf-labels) - Blog of an early ESL hacker.
@@ -7,6 +7,7 @@ exports.assembleLiveContext = assembleLiveContext;
7
7
  exports.assembleExampleContext = assembleExampleContext;
8
8
  const promises_1 = require("fs/promises");
9
9
  const path_1 = __importDefault(require("path"));
10
+ const binding_1 = require("../render/binding");
10
11
  const formatters_1 = require("../render/formatters");
11
12
  const unwrapSignalkTree_1 = require("../render/unwrapSignalkTree");
12
13
  const httpJson_1 = require("../httpJson");
@@ -47,11 +48,19 @@ async function assembleLiveContext(signalkUrl, bindings) {
47
48
  }
48
49
  }
49
50
  const resources = {};
50
- const resourceNames = new Set(bindings.filter((binding) => binding.source === "resources").map((binding) => binding.resource));
51
- for (const name of resourceNames) {
52
- const url = `${signalkUrl}${RESOURCES_API_PATH}/${name}`;
51
+ const resourceBindings = new Map();
52
+ for (const binding of bindings) {
53
+ if (binding.source !== "resources")
54
+ continue;
55
+ resourceBindings.set((0, binding_1.resourceContextKey)(binding), binding);
56
+ }
57
+ for (const [key, binding] of resourceBindings) {
58
+ // A `provider=` binding pins which registered provider plugin answers the request (matches
59
+ // `checkForProvider`'s `?provider=` query param in signalk-server's resources REST API) rather
60
+ // than leaving it to whichever provider the server considers the default for that resource type.
61
+ const url = `${signalkUrl}${RESOURCES_API_PATH}/${binding.resource}${binding.provider ? `?provider=${encodeURIComponent(binding.provider)}` : ""}`;
53
62
  (0, log_1.logDebug)(`GET ${url}`);
54
- resources[name] = await (0, httpJson_1.fetchJson)(url);
63
+ resources[key] = await (0, httpJson_1.fetchJson)(url);
55
64
  }
56
65
  const categoryNames = new Set(bindings.filter((binding) => binding.category).map((binding) => binding.category));
57
66
  const categories = await (0, unitCategories_1.fetchCategoryDisplayUnits)(signalkUrl, categoryNames);
@@ -112,11 +121,18 @@ async function assembleExampleContext(examplesDir, bindings) {
112
121
  }
113
122
  }
114
123
  const resources = {};
115
- const resourceNames = new Set(bindings.filter((binding) => binding.source === "resources").map((binding) => binding.resource));
116
- for (const name of resourceNames) {
117
- const resourcePath = path_1.default.join(examplesDir, "resources", `${name}.json`);
124
+ const resourceBindings = new Map();
125
+ for (const binding of bindings) {
126
+ if (binding.source !== "resources")
127
+ continue;
128
+ resourceBindings.set((0, binding_1.resourceContextKey)(binding), binding);
129
+ }
130
+ for (const [key, binding] of resourceBindings) {
131
+ // Example data has no notion of multiple providers for the same resource type - the file is
132
+ // still named after the bare `resource=` type regardless of any `provider=` a binding pins.
133
+ const resourcePath = path_1.default.join(examplesDir, "resources", `${binding.resource}.json`);
118
134
  (0, log_1.logDebug)(`reading ${resourcePath}`);
119
- resources[name] = await readJsonFile(resourcePath);
135
+ resources[key] = await readJsonFile(resourcePath);
120
136
  }
121
137
  const categoryNames = new Set(bindings.filter((binding) => binding.category).map((binding) => binding.category));
122
138
  let categories = {};
package/dist/config.js CHANGED
@@ -158,7 +158,7 @@ function configSchema(app, discovered = []) {
158
158
  type: "number",
159
159
  title: "Settle time after plugin start (seconds)",
160
160
  description: "Holds off every repaint (startup check, interval, and subscription alike) until this long after the plugin starts - " +
161
- "the first minute or two of SignalK startup is a chaos of plugin dependency sequencing, so data a template needs " +
161
+ "the first minute or two of SignalK startup often has many errors as plugins start before the paths they depend on are ready, so data a template needs " +
162
162
  "(e.g. from the derived-data plugin) may not be published yet.",
163
163
  minimum: 0,
164
164
  default: defaults.settleSeconds,
@@ -10,6 +10,15 @@ export interface Binding {
10
10
  context: string;
11
11
  /** Required when `source === 'resources'` - the Resources API resource type, e.g. `tides`, `waypoints`. */
12
12
  resource?: string;
13
+ /**
14
+ * Only meaningful when `source === 'resources'` - the plugin id of the Resource Provider to query
15
+ * (SignalK's `listResources(resType, params, providerId)`), e.g. `signalk-tides`. Optional - when
16
+ * omitted, the server picks whichever provider is registered for `resource` (its "default"), which is
17
+ * ambiguous/unpredictable when more than one plugin provides the same resource type (e.g. installing
18
+ * a second tides plugin can silently make it the one used instead of the one the template was authored
19
+ * for). Set this to pin a template to one provider regardless of what else is installed.
20
+ */
21
+ provider?: string;
13
22
  /** For `source === 'einklabel'`, a dotted path into the plugin's own injected `meta` (e.g. `repainted`), rather than into vessel/resource data. */
14
23
  path: string;
15
24
  /** A named formatter (see `./formatters.ts`), or `'raw'` to suppress automatic unit conversion (see `renderBinding`). */
@@ -37,6 +46,14 @@ export interface Binding {
37
46
  * unambiguously signals this shorthand.
38
47
  */
39
48
  export declare function parseBinding(desc: string): Binding;
49
+ /**
50
+ * Key under which a `source=resources` binding's data is stored in `context.resources` (see
51
+ * `assembleRawContext` in repaintScheduler.ts and `cli/liveContext.ts`'s equivalent) - incorporates
52
+ * `provider` so two bindings naming the same `resource` type but different providers (e.g. pinning one
53
+ * template's `tides` binding to `provider=signalk-tides` while another is left to the server's default)
54
+ * don't collide on a single cache entry.
55
+ */
56
+ export declare function resourceContextKey(binding: Pick<Binding, "resource" | "provider">): string;
40
57
  /**
41
58
  * Parses every `<text>` and `<image>` element's `<desc>` binding out of raw SVG source - lets a caller
42
59
  * discover what data a template needs before fetching anything, with no separate config declaring it
@@ -1,13 +1,14 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.parseBinding = parseBinding;
4
+ exports.resourceContextKey = resourceContextKey;
4
5
  exports.findBindings = findBindings;
5
6
  exports.resolveBinding = resolveBinding;
6
7
  exports.renderBinding = renderBinding;
7
8
  const xmldom_1 = require("@xmldom/xmldom");
8
9
  const formatters_1 = require("./formatters");
9
10
  const SOURCES = ["signalk", "resources", "einklabel"];
10
- const KNOWN_KEYS = new Set(["source", "context", "resource", "path", "format", "category", "round", "assets"]);
11
+ const KNOWN_KEYS = new Set(["source", "context", "resource", "provider", "path", "format", "category", "round", "assets"]);
11
12
  /**
12
13
  * Parses a `<desc>` element's text content into a `Binding`, e.g.
13
14
  * `source=resources,resource=tides,path=extremes[0].level,category=depth,round=2` or, using the
@@ -45,6 +46,9 @@ function parseBinding(desc) {
45
46
  if (source === "resources" && !fields.resource) {
46
47
  throw new Error(`invalid binding "${desc}" - source=resources requires a "resource" key`);
47
48
  }
49
+ if (fields.provider && source !== "resources") {
50
+ throw new Error(`invalid binding "${desc}" - "provider" is only valid with source=resources`);
51
+ }
48
52
  if (!fields.path) {
49
53
  throw new Error(`invalid binding "${desc}" - missing required "path" key`);
50
54
  }
@@ -52,6 +56,7 @@ function parseBinding(desc) {
52
56
  source,
53
57
  context,
54
58
  resource: fields.resource,
59
+ provider: fields.provider,
55
60
  path: fields.path,
56
61
  format: fields.format,
57
62
  category: fields.category,
@@ -59,6 +64,16 @@ function parseBinding(desc) {
59
64
  assets: fields.assets,
60
65
  };
61
66
  }
67
+ /**
68
+ * Key under which a `source=resources` binding's data is stored in `context.resources` (see
69
+ * `assembleRawContext` in repaintScheduler.ts and `cli/liveContext.ts`'s equivalent) - incorporates
70
+ * `provider` so two bindings naming the same `resource` type but different providers (e.g. pinning one
71
+ * template's `tides` binding to `provider=signalk-tides` while another is left to the server's default)
72
+ * don't collide on a single cache entry.
73
+ */
74
+ function resourceContextKey(binding) {
75
+ return binding.provider ? `${binding.resource}@${binding.provider}` : binding.resource;
76
+ }
62
77
  /**
63
78
  * Parses every `<text>` and `<image>` element's `<desc>` binding out of raw SVG source - lets a caller
64
79
  * discover what data a template needs before fetching anything, with no separate config declaring it
@@ -112,9 +127,10 @@ function resolveBinding(binding, context) {
112
127
  return getAtPath(meta, binding.path);
113
128
  }
114
129
  const resources = context.resources;
115
- const resource = resources?.[binding.resource];
130
+ const resourceKey = resourceContextKey(binding);
131
+ const resource = resources?.[resourceKey];
116
132
  if (resource === undefined) {
117
- throw new Error(`binding references resource "${binding.resource}" which is not present in the render context`);
133
+ throw new Error(`binding references resource "${resourceKey}" which is not present in the render context`);
118
134
  }
119
135
  return getAtPath(resource, binding.path);
120
136
  }
@@ -60,8 +60,9 @@ function stableStringify(value) {
60
60
  }
61
61
  return JSON.stringify(value);
62
62
  }
63
- function hashContext(context) {
64
- return (0, crypto_1.createHash)("sha1").update(stableStringify(context)).digest("hex");
63
+ /** Hashes both the live data context and the template's mtime, so editing a template (even with no data change) triggers a repaint. */
64
+ function hashContext(context, templateMtimeMs) {
65
+ return (0, crypto_1.createHash)("sha1").update(stableStringify(context)).update(String(templateMtimeMs)).digest("hex");
65
66
  }
66
67
  /** Merges `value` into `target` at the nested location described by a dotted SignalK path. */
67
68
  function setAtPath(target, path, value) {
@@ -122,13 +123,20 @@ async function assembleRawContext(app, apiUrl, bindings) {
122
123
  }
123
124
  }
124
125
  const resources = {};
125
- const resourceNames = new Set(bindings.filter((binding) => binding.source === "resources").map((binding) => binding.resource));
126
- for (const name of resourceNames) {
126
+ const resourceBindings = new Map();
127
+ for (const binding of bindings) {
128
+ if (binding.source !== "resources")
129
+ continue;
130
+ resourceBindings.set((0, binding_1.resourceContextKey)(binding), binding);
131
+ }
132
+ for (const [key, binding] of resourceBindings) {
127
133
  // `listResources`'s type only allows the standard SignalKResourceType union, but the underlying
128
134
  // Resources API (and a custom provider like signalk-tides, registered under the non-standard
129
135
  // "tides" type) accepts any registered resource type string - this cast matches `getResource`'s
130
- // wider, accurate signature.
131
- resources[name] = await app.resourcesApi.listResources(name, {});
136
+ // wider, accurate signature. An explicit `provider=` pins which registered provider plugin
137
+ // answers the request (SignalK's `listResources(resType, params, providerId)`) rather than
138
+ // leaving it to whichever provider the server considers the default for that resource type.
139
+ resources[key] = await app.resourcesApi.listResources(binding.resource, {}, binding.provider);
132
140
  }
133
141
  const categoryNames = new Set(bindings.filter((binding) => binding.category).map((binding) => binding.category));
134
142
  if (categoryNames.size > 0 && !apiUrl) {
@@ -155,13 +163,14 @@ async function considerRepaint(app, config, device, state, getApiUrl) {
155
163
  }
156
164
  const templatesDir = (0, config_1.resolveTemplatesDir)(config.templatesDir);
157
165
  const templatePath = (0, config_1.resolveTemplatePath)(templatesDir, device.templateName);
166
+ const templateMtimeMs = (0, fs_1.statSync)(templatePath).mtimeMs;
158
167
  const bindings = (0, binding_1.findBindings)((0, fs_1.readFileSync)(templatePath, "utf-8"));
159
168
  const apiUrl = await getApiUrl().catch((err) => {
160
169
  app.debug(`"${device.friendlyName}": ${err.message}`);
161
170
  return undefined;
162
171
  });
163
172
  const rawContext = await assembleRawContext(app, apiUrl, bindings);
164
- const hash = hashContext(rawContext);
173
+ const hash = hashContext(rawContext, templateMtimeMs);
165
174
  if (state[device.friendlyName]?.hash === hash && !device.forceRepaint) {
166
175
  app.debug(`"${device.friendlyName}": data unchanged, skipping repaint`);
167
176
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rhizomatics/signalk-einklabel-plugin",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "Display SignalK data on eInk Electronic Shelf Labels",
5
5
  "keywords": [
6
6
  "ble",
@@ -69,7 +69,7 @@
69
69
  fill="yellow"
70
70
  style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:32px;font-family:serif;-inkscape-font-specification:'Roboto Serif 20pt';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#ffff00;stroke:#000000;stroke-width:1.2278;stroke-opacity:1"
71
71
  transform="scale(0.89274878,1.1201359)">Tidal Station Name<desc
72
- id="desc1">source=resources,resource=tides,path=station.name</desc></text><text
72
+ id="desc1">source=resources,resource=tides,provider=tides,path=station.name</desc></text><text
73
73
  id="source"
74
74
  x="367.44577"
75
75
  y="226.1548"
@@ -78,7 +78,7 @@
78
78
  fill="black"
79
79
  style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:13.3333px;font-family:sans-serif;-inkscape-font-specification:'\'Roboto\', sans-serif';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#ff0000;stroke-width:3.80045"
80
80
  transform="scale(0.95907862,1.0426674)"><desc
81
- id="desc21">source=resources,resource=tides,path=station.source.name</desc><tspan
81
+ id="desc21">source=resources,resource=tides,provider=tides,path=station.source.name</desc><tspan
82
82
  sodipodi:role="line"
83
83
  id="tspan20"
84
84
  x="367.44577"
@@ -102,7 +102,7 @@
102
102
  font-family="monospace"
103
103
  fill="black"
104
104
  style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:22.6667px;font-family:sans-serif;-inkscape-font-specification:'Roboto, Normal',sans-serif;font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#ff0000"><desc
105
- id="desc13">source=resources,resource=tides,path=extremes[0].label</desc>High or Low</text><text
105
+ id="desc13">source=resources,resource=tides,provider=tides,path=extremes[0].label</desc>High or Low</text><text
106
106
  id="extremes.1"
107
107
  x="7.6000967"
108
108
  y="117.85719"
@@ -110,7 +110,7 @@
110
110
  font-family="monospace"
111
111
  fill="black"
112
112
  style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:22.6667px;font-family:sans-serif;-inkscape-font-specification:'Roboto, Normal',sans-serif;font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#ff0000"><desc
113
- id="desc14">source=resources,resource=tides,path=extremes[1].label</desc>High or Low</text><text
113
+ id="desc14">source=resources,resource=tides,provider=tides,path=extremes[1].label</desc>High or Low</text><text
114
114
  id="extremes.2"
115
115
  x="7.2470098"
116
116
  y="176.12112"
@@ -118,7 +118,7 @@
118
118
  font-family="monospace"
119
119
  fill="red"
120
120
  style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:22.6667px;font-family:sans-serif;-inkscape-font-specification:'Roboto, Normal',sans-serif;font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#ff0000"><desc
121
- id="desc15">source=resources,resource=tides,path=extremes[2].label</desc>High or Low</text><text
121
+ id="desc15">source=resources,resource=tides,provider=tides,path=extremes[2].label</desc>High or Low</text><text
122
122
  id="timezone"
123
123
  x="330.60489"
124
124
  y="204.67754"
@@ -148,7 +148,7 @@
148
148
  font-family="sans-serif"
149
149
  fill="#ffff00"
150
150
  style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:21.3333px;font-family:sans-serif;-inkscape-font-specification:'\'Roboto, Normal\', sans-serif';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#1a1a1a;stroke:#fefe54;stroke-opacity:1"><desc
151
- id="desc18">source=resources,resource=tides,path=station.datums.LAT,category=depth,round=1</desc><tspan
151
+ id="desc18">source=resources,resource=tides,provider=tides,path=station.datums.LAT,category=depth,round=1</desc><tspan
152
152
  sodipodi:role="line"
153
153
  id="tspan17"
154
154
  x="282.65424"
@@ -160,7 +160,7 @@
160
160
  font-family="sans-serif"
161
161
  fill="#ffff00"
162
162
  style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:21.3333px;font-family:sans-serif;-inkscape-font-specification:'\'Roboto, Normal\', sans-serif';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#1a1a1a;stroke:#fefe54;stroke-opacity:1"><desc
163
- id="desc19">source=resources,resource=tides,path=station.datums.HAT,category=depth,round=1</desc><tspan
163
+ id="desc19">source=resources,resource=tides,provider=tides,path=station.datums.HAT,category=depth,round=1</desc><tspan
164
164
  sodipodi:role="line"
165
165
  id="tspan18"
166
166
  x="360.77148"
@@ -180,7 +180,7 @@
180
180
  x="322.52386"
181
181
  y="158.08005"
182
182
  style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:37.3333px;font-family:monospace;-inkscape-font-specification:'\'Roboto Mono, Bold\', monospace';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke-width:1.78893">3.3m</tspan><desc
183
- id="desc6">source=resources,resource=tides,path=extremes[2].level,category=depth</desc></text><text
183
+ id="desc6">source=resources,resource=tides,provider=tides,path=extremes[2].level,category=depth</desc></text><text
184
184
  id="extremes.2.time"
185
185
  x="203.77321"
186
186
  y="146.1584"
@@ -189,7 +189,7 @@
189
189
  fill="red"
190
190
  style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:37.3333px;font-family:monospace;-inkscape-font-specification:'\'Roboto Mono, Bold\', monospace';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#000000;stroke-width:1.51352"
191
191
  transform="scale(0.70226239,1.4239692)">15:53<desc
192
- id="desc10">source=resources,resource=tides,path=extremes[2].time,format=local_time</desc></text><text
192
+ id="desc10">source=resources,resource=tides,provider=tides,path=extremes[2].time,format=local_time</desc></text><text
193
193
  id="extremes.1.level"
194
194
  x="334.30084"
195
195
  y="109.17664"
@@ -205,7 +205,7 @@
205
205
  x="334.30084"
206
206
  y="109.17664"
207
207
  style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:37.3333px;font-family:monospace;-inkscape-font-specification:'\'Roboto Mono, Bold\', monospace';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke-width:1.701">1.6m</tspan><desc
208
- id="desc5">source=resources,resource=tides,path=extremes[1].level,category=depth</desc></text><text
208
+ id="desc5">source=resources,resource=tides,provider=tides,path=extremes[1].level,category=depth</desc></text><text
209
209
  id="extremes.1.time"
210
210
  x="188.63072"
211
211
  y="108.65867"
@@ -214,7 +214,7 @@
214
214
  fill="red"
215
215
  style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:37.3333px;font-family:monospace;-inkscape-font-specification:'\'Roboto Mono, Bold\', monospace';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#000000;stroke-width:1.48364"
216
216
  transform="scale(0.7345432,1.3613903)">09:36<desc
217
- id="desc8">source=resources,resource=tides,path=extremes[1].time,format=local_time</desc></text><text
217
+ id="desc8">source=resources,resource=tides,provider=tides,path=extremes[1].time,format=local_time</desc></text><text
218
218
  id="extremes.0.time"
219
219
  x="184.40854"
220
220
  y="64.242691"
@@ -223,7 +223,7 @@
223
223
  fill="red"
224
224
  style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:37.3333px;font-family:monospace;-inkscape-font-specification:'Roboto Mono, Bold',monospace;font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#000000;stroke-width:1.55397"
225
225
  transform="scale(0.74654856,1.3394976)">03:27<desc
226
- id="desc3">source=resources,resource=tides,path=extremes[0].time,format=local_time</desc></text><text
226
+ id="desc3">source=resources,resource=tides,provider=tides,path=extremes[0].time,format=local_time</desc></text><text
227
227
  id="extremes.0.date"
228
228
  x="10.326324"
229
229
  y="74.630478"
@@ -232,7 +232,7 @@
232
232
  fill="red"
233
233
  style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:22.6667px;font-family:sans-serif;-inkscape-font-specification:'Roboto, Normal',sans-serif;font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#666666;stroke-width:1.32646"
234
234
  transform="scale(0.82428216,1.2131768)"><desc
235
- id="desc2">source=resources,resource=tides,path=extremes[0].time,format=day_mon</desc>27 Jun</text><text
235
+ id="desc2">source=resources,resource=tides,provider=tides,path=extremes[0].time,format=day_mon</desc>27 Jun</text><text
236
236
  id="extremes.1.date"
237
237
  x="9.9649849"
238
238
  y="123.34451"
@@ -241,7 +241,7 @@
241
241
  fill="red"
242
242
  style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:22.6667px;font-family:sans-serif;-inkscape-font-specification:'Roboto, Normal',sans-serif;font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#666666;stroke-width:1.32646"
243
243
  transform="scale(0.82428216,1.2131768)"><desc
244
- id="desc7">source=resources,resource=tides,path=extremes[1].time,format=day_mon</desc>27 Jun</text><text
244
+ id="desc7">source=resources,resource=tides,provider=tides,path=extremes[1].time,format=day_mon</desc>27 Jun</text><text
245
245
  id="extremes.2.date"
246
246
  x="12.934491"
247
247
  y="156.77794"
@@ -250,7 +250,7 @@
250
250
  fill="red"
251
251
  style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:22.6667px;font-family:sans-serif;-inkscape-font-specification:'Roboto, Normal',sans-serif;font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#666666;stroke-width:1.4652"
252
252
  transform="scale(0.74623156,1.3400666)"><desc
253
- id="desc9">source=resources,resource=tides,path=extremes[2].time,format=day_mon</desc>27 Jun</text><text
253
+ id="desc9">source=resources,resource=tides,provider=tides,path=extremes[2].time,format=day_mon</desc>27 Jun</text><text
254
254
  id="extremes.0.level"
255
255
  x="324.0769"
256
256
  y="64.82357"
@@ -264,7 +264,7 @@
264
264
  x="324.0769"
265
265
  y="64.82357"
266
266
  style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:37.3333px;font-family:monospace;-inkscape-font-specification:'\'Roboto Mono, Bold\', monospace';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke-width:1.71992">3.4m</tspan><desc
267
- id="desc4">source=resources,resource=tides,path=extremes[0].level,category=depth</desc></text><text
267
+ id="desc4">source=resources,resource=tides,provider=tides,path=extremes[0].level,category=depth</desc></text><text
268
268
  xml:space="preserve"
269
269
  style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:21.3333px;font-family:sans-serif;-inkscape-font-specification:'\'Roboto, Normal\', sans-serif';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#1a1a1a;stroke:#fefe54;stroke-width:0.612283;stroke-opacity:1"
270
270
  x="337.254"