@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 +6 -1
- package/README.md +28 -28
- package/dist/cli/liveContext.js +24 -8
- package/dist/config.js +1 -1
- package/dist/render/binding.d.ts +17 -0
- package/dist/render/binding.js +19 -3
- package/dist/repaintScheduler.js +16 -7
- package/package.json +1 -1
- package/templates/tide.svg +16 -16
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
|
|
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
|

|
|
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.
|
|
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
|
|
276
|
-
station.name
|
|
277
|
-
source.name.
|
|
278
|
-
last_repaint
|
|
279
|
-
extremes.0
|
|
280
|
-
extremes.1
|
|
281
|
-
extremes.2
|
|
282
|
-
timezoneRegion
|
|
283
|
-
lat
|
|
284
|
-
hat
|
|
285
|
-
extremes.2.level
|
|
286
|
-
extremes.2.time
|
|
287
|
-
extremes.1.level
|
|
288
|
-
extremes.1.time
|
|
289
|
-
extremes.0.time
|
|
290
|
-
extremes.0.time-8
|
|
291
|
-
extremes.0.time-8-5
|
|
292
|
-
extremes.0.time-8-9
|
|
293
|
-
extremes.0.level
|
|
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
|
|
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) -
|
|
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.
|
package/dist/cli/liveContext.js
CHANGED
|
@@ -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
|
|
51
|
-
for (const
|
|
52
|
-
|
|
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[
|
|
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
|
|
116
|
-
for (const
|
|
117
|
-
|
|
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[
|
|
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
|
|
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,
|
package/dist/render/binding.d.ts
CHANGED
|
@@ -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
|
package/dist/render/binding.js
CHANGED
|
@@ -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
|
|
130
|
+
const resourceKey = resourceContextKey(binding);
|
|
131
|
+
const resource = resources?.[resourceKey];
|
|
116
132
|
if (resource === undefined) {
|
|
117
|
-
throw new Error(`binding references resource "${
|
|
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
|
}
|
package/dist/repaintScheduler.js
CHANGED
|
@@ -60,8 +60,9 @@ function stableStringify(value) {
|
|
|
60
60
|
}
|
|
61
61
|
return JSON.stringify(value);
|
|
62
62
|
}
|
|
63
|
-
|
|
64
|
-
|
|
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
|
|
126
|
-
for (const
|
|
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
|
-
|
|
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
package/templates/tide.svg
CHANGED
|
@@ -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"
|