frugal-iot-client 1.3.11
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/.claude/settings.local.json +28 -0
- package/CLAUDE.md +263 -0
- package/HEADLESS_PLAN.md +167 -0
- package/LICENSE +21 -0
- package/README.md +48 -0
- package/dashboard_example.html +458 -0
- package/frugaliot.css +271 -0
- package/images/Battery0.png +0 -0
- package/images/Battery1.png +0 -0
- package/images/Battery2.png +0 -0
- package/images/Battery3.png +0 -0
- package/images/Battery4.png +0 -0
- package/images/Battery5.png +0 -0
- package/images/Battery6.png +0 -0
- package/images/icon_graph.svg +1 -0
- package/index-embedded.html +56 -0
- package/index-project.html +26 -0
- package/index.html +36 -0
- package/login.html +27 -0
- package/package.json +34 -0
- package/webcomponents.js +5003 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Bash(node --check webcomponents.js)",
|
|
5
|
+
"Bash(node --check /Users/mitra/git/github_mitra42/frugal-iot-client/webcomponents.js)",
|
|
6
|
+
"Bash(git -C /Users/mitra/git/github_mitra42/frugal-iot-client status)",
|
|
7
|
+
"Bash(git -C /Users/mitra/git/github_mitra42/frugal-iot-client stash list)",
|
|
8
|
+
"Bash(git -C /Users/mitra/git/github_mitra42/frugal-iot-client show dashboard_example2:dashboard_example2.html)",
|
|
9
|
+
"Bash(git -C /Users/mitra/git/github_mitra42/frugal-iot-client branch -a)",
|
|
10
|
+
"Bash(git -C /Users/mitra/git/github_mitra42/frugal-iot-client show 73-headless:dashboard_example2.html)",
|
|
11
|
+
"Bash(git *)",
|
|
12
|
+
"WebFetch(domain:github.com)",
|
|
13
|
+
"Bash(curl -sL -H \"Accept: image/svg+xml\" \"https://github.com/user-attachments/assets/9d83e51f-a392-454f-860d-a855658ccdfb\")",
|
|
14
|
+
"Bash(curl -sL \"https://github.com/user-attachments/assets/9d83e51f-a392-454f-860d-a855658ccdfb?raw=true\")",
|
|
15
|
+
"Bash(curl -sL \"https://github.com/user-attachments/assets/9d83e51f-a392-454f-860d-a855658ccdfb\")",
|
|
16
|
+
"Read(//tmp/**)",
|
|
17
|
+
"Bash(curl -s -I \"https://github.com/user-attachments/assets/9d83e51f-a392-454f-860d-a855658ccdfb\")",
|
|
18
|
+
"Bash(curl -s -I \"https://github.com/user-attachments/assets/6c6bc26c-bf02-4def-a6f2-1e0ee00aa399\")",
|
|
19
|
+
"Bash(curl -s \"https://github-production-user-asset-6210df.s3.amazonaws.com/2242709/501137168-f4cc6bf7-0c67-4e9c-b597-7a17c155bb3a.svg\")",
|
|
20
|
+
"Bash(curl -s \"https://github-production-user-asset-6210df.s3.amazonaws.com/2242709/604453755-9d83e51f-a392-454f-860d-a855658ccdfb.svg\")",
|
|
21
|
+
"Bash(curl -s \"https://github-production-user-asset-6210df.s3.amazonaws.com/2242709/604454647-6c6bc26c-bf02-4def-a6f2-1e0ee00aa399.svg\")",
|
|
22
|
+
"Bash(curl *)",
|
|
23
|
+
"Bash(grep -n \"el\\('p', {}, \\\\[\\\\\"\" webcomponents.js)",
|
|
24
|
+
"Bash(grep -n \"el\\('h[1-5]', {}, \\\\[\\\\\"\" webcomponents.js)",
|
|
25
|
+
"Bash(grep -n \"^const languages\\\\|^EN:\\\\|^FR:\\\\|^HI:\\\\|^ID:\\\\|^\\\\`\\);\" /Users/mitra/git/github_mitra42/frugal-iot-client/webcomponents.js)"
|
|
26
|
+
]
|
|
27
|
+
}
|
|
28
|
+
}
|
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
# Frugal IoT Client
|
|
2
|
+
|
|
3
|
+
JavaScript/Web Components MQTT client for the Frugal IoT project. Connects to an MQTT broker, auto-discovers nodes via discovery messages, and renders live sensor data using custom HTML elements.
|
|
4
|
+
|
|
5
|
+
## Main file
|
|
6
|
+
|
|
7
|
+
`webcomponents.js` — the entire client lives here (~3600 lines). All web components, MQTT logic, graphing, i18n, and admin UI are defined in this one file.
|
|
8
|
+
|
|
9
|
+
Dashboards (e.g. `dashboard_example2.html`) are thin HTML pages that import selectively from `webcomponents.js`.
|
|
10
|
+
|
|
11
|
+
## Code style
|
|
12
|
+
|
|
13
|
+
- **Indentation**: 2 spaces throughout.
|
|
14
|
+
- **Async style**: prefer callback-style async (using the `async` library from `caolan/async`) over Promise chains. Existing `fetch` calls that use `.then()` are legacy patterns; new async code should use callbacks or `async.waterfall` / `async.parallel` etc.
|
|
15
|
+
- **Comments**: explain WHY, not WHAT. One short line is enough; do not restate what the code clearly shows. No multi-line docstrings.
|
|
16
|
+
- Good: `// topicSetPath throws when this.node is null — guard before calling`
|
|
17
|
+
- Bad: `// Check if topic starts with the set path`
|
|
18
|
+
- **No trailing summaries** — do not add a comment block or prose after a method explaining what it does.
|
|
19
|
+
- **Getters**: prefer `get foo()` over plain property access whenever a value is derived or retrieved by traversing connected objects (e.g. `this.node.project`, `this.parentElement`, `this.mt.node.groups[this.group]`). Getters make the dependency chain explicit and let callers read `thing.project` naturally.
|
|
20
|
+
- **Dashboard functionality**: prefer adding reusable behaviour to `webcomponents.js` over implementing it inside a custom dashboard file. If a dashboard needs to traverse the topic tree, compute a list, or fire an event, that logic belongs on the relevant class as a getter or method.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Naming conventions
|
|
25
|
+
|
|
26
|
+
### Topic path segments
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
org / project / nodeId / group / leaf read
|
|
30
|
+
org / project / nodeId / set / group / leaf write
|
|
31
|
+
org / project / nodeId / set / group / leaf / wired wiring
|
|
32
|
+
org / project / nodeId / set / group / leaf / parameter parameter (e.g. max, min)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Concrete examples:
|
|
36
|
+
```
|
|
37
|
+
dev/myproject/node42/sht/temperature
|
|
38
|
+
dev/myproject/node42/set/sht/temperature
|
|
39
|
+
dev/myproject/node42/set/sht/temperature/wired
|
|
40
|
+
dev/myproject/node42/set/sht/temperature/max
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
| Term | Meaning | Example |
|
|
44
|
+
|---|---|---|
|
|
45
|
+
| `path` / `topicPath` | Full path from root | `dev/myproject/node42/sht/temperature` |
|
|
46
|
+
| `twig` | From group onward; strips org/project/node prefix and `set/` | `sht/temperature` |
|
|
47
|
+
| `leaf` | Last segment only | `temperature` |
|
|
48
|
+
| `parameter` | Sub-path under the set path | `max`, `min`, `wired` |
|
|
49
|
+
|
|
50
|
+
Helper functions: `topicTwig(path)` → twig, `topicLeaf(path)` → leaf, `twigAttribute(path)` → `sht_temperature`, `leafAttribute(path)` → `temperature_max` (slashes replaced with underscores for DOM attributes).
|
|
51
|
+
|
|
52
|
+
### Variable suffixes
|
|
53
|
+
|
|
54
|
+
| Suffix | Points to | Example |
|
|
55
|
+
|---|---|---|
|
|
56
|
+
| `Mt` | An `MqttTopic` instance | `sensorMt`, `nodeMt`, `groupMt`, `projectMt` |
|
|
57
|
+
| `Twig` | A string starting at group/leaf | `sht/temperature` |
|
|
58
|
+
| `Leaf` | A string holding just the final segment | `temperature` |
|
|
59
|
+
| `Path` | A full MQTT path string | `dev/myproject/node42/sht/temperature` |
|
|
60
|
+
| `El` | A DOM element | `barEl`, `groupEl` |
|
|
61
|
+
|
|
62
|
+
### Object properties on `MqttTopic`
|
|
63
|
+
|
|
64
|
+
- `mt.twig` — stored twig string (e.g. `sht/temperature`)
|
|
65
|
+
- `mt.leaf` — getter, last segment of twig
|
|
66
|
+
- `mt.topicPath` — getter, full path including node prefix
|
|
67
|
+
- `mt.topicSetPath` — getter, write path (`…/set/…`); **throws when `this.node` is null** — only call on topics that are known to be part of the node hierarchy (i.e. `this.node` is set). Project-level or standalone topics (e.g. those created by an embedded `mqtt-graphdataset`) have no real node and must not reach this getter.
|
|
68
|
+
- `mt.element` — back-reference to the DOM element currently bound to this topic (null in headless mode or before binding)
|
|
69
|
+
- `el.mt` — forward-reference from a DOM element to its `MqttTopic`
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Architecture
|
|
74
|
+
|
|
75
|
+
### Global state (module-level)
|
|
76
|
+
|
|
77
|
+
| Variable | Purpose |
|
|
78
|
+
|---|---|
|
|
79
|
+
| `mqtt_client` | The live MQTT connection (mqtt.js) |
|
|
80
|
+
| `mqtt_subscriptions` | `[{topic, cb}]` — all active subscriptions |
|
|
81
|
+
| `server_config` | Config JSON fetched from `/config.json` at startup |
|
|
82
|
+
| `graph` | The default `MqttGraph` created on first graphing action |
|
|
83
|
+
| `preferedLanguages` | Array of language codes, e.g. `['EN']` |
|
|
84
|
+
|
|
85
|
+
### Class hierarchy
|
|
86
|
+
|
|
87
|
+
Data-tree classes are always instantiated. UI element classes are only instantiated when not in headless mode.
|
|
88
|
+
|
|
89
|
+
**Data-tree** (plain JS, no HTMLElement):
|
|
90
|
+
```
|
|
91
|
+
MqttTopic leaf data node — history, metadata, subscribe/publish logic
|
|
92
|
+
(future: MqttTopicProject → MqttTopicNode → MqttTopicGroup, see HEADLESS_PLAN.md)
|
|
93
|
+
Watchdog offline detection timer per node
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
**UI elements** (extend HTMLElementExtended):
|
|
97
|
+
```
|
|
98
|
+
HTMLElementExtended (from html-element-extended)
|
|
99
|
+
MqttElement attributeChangedCallback, typeOfAttribute
|
|
100
|
+
MqttReceiver topicValueSet, valueSet, parameterSet, render helpers
|
|
101
|
+
MqttTransmitter publish, valueGet
|
|
102
|
+
MqttText text/number input
|
|
103
|
+
MqttColor color picker input
|
|
104
|
+
MqttToggle boolean checkbox
|
|
105
|
+
MqttSlider drag slider
|
|
106
|
+
MqttBar progress bar display
|
|
107
|
+
MqttGauge dial-gauge display
|
|
108
|
+
MqttProject manages a project and its nodes ← not created in headless
|
|
109
|
+
MqttNode manages a single IoT node ← not created in headless
|
|
110
|
+
MqttChooseTopic dropdown to wire one topic to another
|
|
111
|
+
MqttGraph Chart.js wrapper (time-series graph)
|
|
112
|
+
MqttGraphDataset one dataset/line within a MqttGraph
|
|
113
|
+
MqttGroup collapsible group of topics ← not created in headless
|
|
114
|
+
MqttSummaryGroup base for groups with a summary line
|
|
115
|
+
MqttGroupLedbuiltin LED on/off indicator dot
|
|
116
|
+
MqttGroupRelay relay on/off symbol
|
|
117
|
+
MqttGroupSht temperature + humidity summary
|
|
118
|
+
MqttGroupSoil soil moisture %
|
|
119
|
+
MqttGroupDS18B20 DS18B20 temperature °C
|
|
120
|
+
MqttGroupBattery battery mV
|
|
121
|
+
MqttGroupOta OTA key display
|
|
122
|
+
MqttGroupControlHysteresis control loop summary
|
|
123
|
+
MqttGroupControlHysterisis legacy spelling alias
|
|
124
|
+
MqttGroupFrugalIot the frugal_iot system group (name, battery, OTA…)
|
|
125
|
+
MqttClient MQTT connection UI element
|
|
126
|
+
MqttWrapper top-level entry point; fetches config, builds tree
|
|
127
|
+
MqttLogin login/register form
|
|
128
|
+
MqttAdmin admin panel (OTA, permissions, node table)
|
|
129
|
+
LanguagePicker language selector
|
|
130
|
+
TabbedDisplay generic tab UI
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Adding a new display element
|
|
134
|
+
|
|
135
|
+
1. Create a class extending `MqttReceiver` (read-only) or `MqttTransmitter` (read/write).
|
|
136
|
+
2. Override `valueSet(val)` to update the DOM in-place (set `elements.*` refs in `render()`); return `false` to skip full re-render.
|
|
137
|
+
3. Override `render()` to return the initial shadow-DOM tree using the local `el()` helper.
|
|
138
|
+
4. Register with `customElements.define('mqtt-<name>', MyClass)`.
|
|
139
|
+
5. Add a `case '<display-name>':` in `MqttTopic.createElement()`.
|
|
140
|
+
|
|
141
|
+
### Adding a new group summary
|
|
142
|
+
|
|
143
|
+
1. Extend `MqttSummaryGroup`.
|
|
144
|
+
2. Declare `static get observedAttributes()` and the matching type arrays (`floatAttributes`, `boolAttributes`, etc.).
|
|
145
|
+
3. Implement `summaryText()` returning a short string.
|
|
146
|
+
4. Register as `mqtt-group<groupid>` — `MqttNode.addGroupFromTemplate` will pick it up automatically.
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Headless mode
|
|
151
|
+
|
|
152
|
+
`<mqtt-wrapper headless>` builds the full data tree without creating any DOM elements for the project/node/group/leaf UX. Dashboards subscribe to document events and create individual standalone elements on demand.
|
|
153
|
+
|
|
154
|
+
Key points for `MqttTopic.message_received` headless path (the `else` branch when `this.element` is null):
|
|
155
|
+
- Must set `this.state.value` so that getters like `usableName` and `wiredTopic` work without a DOM element.
|
|
156
|
+
- Must call `setWired` when a `wired` parameter arrives.
|
|
157
|
+
- Must fire `frugaliot:groupchanged` to notify dashboards of state changes.
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## Dashboard patterns
|
|
162
|
+
|
|
163
|
+
### Prefer getters on `MqttTopic` over dashboard logic
|
|
164
|
+
|
|
165
|
+
When a dashboard needs derived data (usable name, min/max, wired source), add a `get` on `MqttTopic` rather than computing it in the dashboard. Dashboards should just read `mt.someGetter`.
|
|
166
|
+
|
|
167
|
+
### Binding elements to data-tree topics
|
|
168
|
+
|
|
169
|
+
Always pre-bind before `appendChild`, otherwise `connectedCallback` calls `createTopic()` and fabricates a wrong-path subscription:
|
|
170
|
+
|
|
171
|
+
```javascript
|
|
172
|
+
const barEl = el('mqtt-bar', { topic: sensorMt.topicPath, ... });
|
|
173
|
+
barEl.mt = sensorMt; // prevents createTopic() from running
|
|
174
|
+
sensorMt.element = barEl; // routes wildcard subscription messages to this element
|
|
175
|
+
barContainer.appendChild(barEl);
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Clear back-references when rebuilding so stale elements don't receive messages:
|
|
179
|
+
|
|
180
|
+
```javascript
|
|
181
|
+
if (prevBarMt) prevBarMt.element = null;
|
|
182
|
+
prevBarMt = sensorMt;
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Looking up topics
|
|
186
|
+
|
|
187
|
+
Use `wrapper.projectMt.findTopic(fullPath)` — never fabricate a new `MqttTopic` with partial paths:
|
|
188
|
+
|
|
189
|
+
```javascript
|
|
190
|
+
const mt = document.querySelector('mqtt-wrapper')?.projectMt?.findTopic(this.state.topic);
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Events
|
|
194
|
+
|
|
195
|
+
| Event | Fired by | Carries | Typical use |
|
|
196
|
+
|---|---|---|---|
|
|
197
|
+
| `frugaliot:controlgroup` | `MqttGroupControlHysteresis` | `{ nodeMt, groupId, topics }` | populate node/group dropdowns |
|
|
198
|
+
| `frugaliot:groupchanged` | `MqttTopic.message_received` (headless) | `{ nodeMt, groupId, groupMt, changed }` | re-run `wireUpDashboard` on wired/on state changes |
|
|
199
|
+
| `frugaliot:topicschanged` | `MqttProject` | `{ project }` | retry wiring after late-arriving nodes |
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## Internationalisation
|
|
204
|
+
|
|
205
|
+
The master language table is `const languages` in `webcomponents.js`. Dashboard-specific strings are added via `addVocabulary(yamlString)` in the dashboard's `<script>`.
|
|
206
|
+
|
|
207
|
+
Every `addVocabulary` block must include **all four** language sections (EN, FR, HI, ID). Strings absent from `webcomponents.js` must be explicitly added in the dashboard block — do not assume the core file covers them.
|
|
208
|
+
|
|
209
|
+
### How a string gets translated
|
|
210
|
+
|
|
211
|
+
There are three ways a piece of UI text is translated. If a string doesn't go through one of these, it silently stays in English regardless of the selected language:
|
|
212
|
+
|
|
213
|
+
1. **Direct call to `getString(tag)`** — looks the string up in `languages`, falling back to the English value, falling back to `tag` itself. Used for names built dynamically (e.g. a graph's scale/axis name at `text: getString(this.name...)`).
|
|
214
|
+
2. **Via `el(tag, attributes, children)`** — `el()` auto-translates certain *attributes* on certain *tags*, per the `i8ntags` table (currently `label`, `button`, `span`, `option`, `p`, `h1`–`h5`, `th`, all via their `textContent` attribute). Add a tag/attribute pair to `i8ntags` when a new kind of element needs translated text. Excluded even on a listed tag: values containing `:` or `/` (these are usually paths or key:value pairs, not prose), and values not starting with a letter (emoji/symbol-only content). Pass `i8n: false` on an element you know is untranslatable (a proper name, an id, dynamic per-row data).
|
|
215
|
+
3. **Via a graph's scale name** — same as (1), a direct `getString()` call when building the Chart.js scale config.
|
|
216
|
+
|
|
217
|
+
**Only `textContent` is translated — never a literal string passed as a DOM `children` argument.** `el('p', {}, ["Some text"])` bypasses the mechanism entirely because the filter inspects `attributes`, not `children`. Always write `el('p', {textContent: "Some text"})` instead. This applies to `p`, `h1`–`h5`, `span`, and any other tag in `i8ntags`.
|
|
218
|
+
|
|
219
|
+
When adding a new UI string: add matching entries to all four language sections in `languages` (or the dashboard's `addVocabulary` block) — even if you only have the English text, add the same text under EN so `getString` doesn't silently fall through and hide missing translations.
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## Key helpers
|
|
224
|
+
|
|
225
|
+
- `el(tag, attributes, children)` — local wrapper around `EL` that applies i18n translation to label/button/span text. Pass `i8n: false` in attributes to suppress translation for names/IDs.
|
|
226
|
+
- `getString(tag)` — look up a string in the current language(s); falls back to English.
|
|
227
|
+
- `mqtt_subscribe(topic, cb)` — registers a subscription; replays on reconnect.
|
|
228
|
+
- `GET(url, opts, cb)` — from `html-element-extended`; callback-style HTTP GET returning parsed JSON.
|
|
229
|
+
- `XXX(args)` — debug log with a breakpoint hook; use for unexpected states. Leave calls in place.
|
|
230
|
+
- `XXY(args)` — debug log for known-legacy paths; returns `false` so it can be used in `&&` guards. Don't add new calls.
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## Dependencies
|
|
235
|
+
|
|
236
|
+
| Package | Role |
|
|
237
|
+
|---|---|
|
|
238
|
+
| `html-element-extended` | Base class `HTMLElementExtended`, `EL`, `GET`, `toBool` |
|
|
239
|
+
| `mqtt` | MQTT over WebSocket |
|
|
240
|
+
| `js-yaml` | Parse YAML in discovery messages and i18n config |
|
|
241
|
+
| `async` | Callback-style async utilities (preferred over Promises) |
|
|
242
|
+
| `csv-parse` | Parse historical CSV data from the logger |
|
|
243
|
+
| `chart.js` | Time-series graphs |
|
|
244
|
+
| `luxon` | Date/time for Chart.js (adapter inlined in webcomponents.js) |
|
|
245
|
+
| `dial-gauge` | Gauge web component |
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
## Running locally
|
|
250
|
+
|
|
251
|
+
```bash
|
|
252
|
+
npm install
|
|
253
|
+
# run frugal-iot-server pointing htmldir at this directory
|
|
254
|
+
node ../frugal-iot-server/frugal-iot-server.js
|
|
255
|
+
# open https://localhost:8080
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
## Debugging tips
|
|
259
|
+
|
|
260
|
+
- Sprinkle `XXX("label")` calls and set a breakpoint inside `XXX` to trace execution.
|
|
261
|
+
- `XXY` is for legacy twigs that should eventually disappear — don't add new `XXY` calls.
|
|
262
|
+
- `this.state.elements` holds named refs to live DOM nodes updated without full re-render.
|
|
263
|
+
- `server_config` (fetched from `/config.json`) drives schema, modules, topics, and org/project/node hierarchy.
|
package/HEADLESS_PLAN.md
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# Plan: Headless Data-Tree Mode for mqtt-wrapper
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
Allow `mqtt-wrapper` to operate without building any DOM elements for the project/node/group/leaf UX. The `MqttTopic` data tree becomes the single source of truth, shared across all dashboards. DOM elements become optional UX decorations layered on top. In headless mode, a dashboard may still create individual standalone elements by calling `MqttTopic.createElement()` directly.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## New Class Hierarchy
|
|
10
|
+
|
|
11
|
+
Four data-tree classes, all subclasses of `MqttTopic`, paired with optional UX element classes:
|
|
12
|
+
|
|
13
|
+
| Data-tree class | UX element class | Contains |
|
|
14
|
+
|---|---|---|
|
|
15
|
+
| `MqttTopicProject` | `MqttProject` | `nodes: { nodeId → MqttTopicNode }` |
|
|
16
|
+
| `MqttTopicNode` | `MqttNode` | `groups: { groupId → MqttTopicGroup }` |
|
|
17
|
+
| `MqttTopicGroup` | `MqttGroup` (and subclasses) | `topics: { leaf → MqttTopic }` |
|
|
18
|
+
| `MqttTopic` | `MqttBar`, `MqttText`, etc. | `data[]` (time-series history) |
|
|
19
|
+
|
|
20
|
+
In full-UI mode all four levels have both a data object and a paired element. In headless mode only the data objects are created; individual elements can still be created on demand via `MqttTopic.createElement()`.
|
|
21
|
+
|
|
22
|
+
The tree structure makes the data hierarchy explicit and matches the MQTT topic path structure:
|
|
23
|
+
```
|
|
24
|
+
org / project / nodeId / groupId / leaf
|
|
25
|
+
Project Node Group Topic
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Proposed Changes
|
|
31
|
+
|
|
32
|
+
### 1. Add a `headless` attribute to `mqtt-wrapper`
|
|
33
|
+
|
|
34
|
+
When set, the wrapper does not build any DOM for project/node/group/leaf display. The `mqtt-client` connection UI still renders (unless separately suppressed).
|
|
35
|
+
|
|
36
|
+
In headless mode:
|
|
37
|
+
- `MqttTopicProject` is created but `MqttProject` (the element) is not.
|
|
38
|
+
- `MqttTopicNode` is created but `MqttNode` (the element) is not.
|
|
39
|
+
- `MqttTopicGroup` is created but `MqttGroup` (the element) is not.
|
|
40
|
+
- Leaf `MqttTopic` objects are created as normal — the data tree is fully populated.
|
|
41
|
+
- A dashboard can still call `mt.createElement()` on any individual leaf `MqttTopic` to get a standalone renderable element for that topic.
|
|
42
|
+
|
|
43
|
+
`headless` propagates from `mqtt-wrapper` → `MqttTopicProject` → `MqttTopicNode` → `MqttTopicGroup` so each level knows whether to instantiate its paired element.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
### 2. Introduce `MqttTopicGroup` and restructure the topic index
|
|
48
|
+
|
|
49
|
+
Currently `MqttTopicNode` holds a flat map of all leaf topics keyed by twig (`group/leaf`). This is replaced by a two-level structure:
|
|
50
|
+
|
|
51
|
+
- `MqttTopicNode.groups[groupId]` → `MqttTopicGroup`
|
|
52
|
+
- `MqttTopicGroup.topics[leaf]` → `MqttTopic`
|
|
53
|
+
|
|
54
|
+
`MqttTopicGroup` is a subclass of `MqttTopic`. It holds:
|
|
55
|
+
- `topics` — the leaf topics for this group
|
|
56
|
+
- State fields for the group (name, `on`, `now_wired`, `out_wired`, etc.) — see change 3
|
|
57
|
+
|
|
58
|
+
**Impact on `findTopic`** (currently line 2898): navigates directly through the grouped structure:
|
|
59
|
+
```javascript
|
|
60
|
+
// old: this.state.nodes[nodeId].state.topics[`${group}/${leaf}/#`]
|
|
61
|
+
// new:
|
|
62
|
+
const node = this.nodes[nodeId]; // MqttTopicNode
|
|
63
|
+
const group = node && node.groups[groupId]; // MqttTopicGroup
|
|
64
|
+
return group && group.topics[leaf]; // MqttTopic
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
**Impact on `controlGroups` getter** (currently derives groups by filtering the flat topics map): now reads directly from `MqttTopicNode.groups` filtered by `groupId.startsWith('control')` — simpler and no derivation needed.
|
|
68
|
+
|
|
69
|
+
**Impact on `sendMessageToMatchingTopics` / MQTT routing**: currently iterates `state.topics[twig + "/#"]` with wildcard matching. With two-level storage, routing splits the twig on `/` to reach `groups[groupId].topics[leaf]` directly, which is O(1) rather than a scan. The wildcard case (messages arriving at sub-paths like `control/now/wired`) is handled by looking up `groups[groupId].topics[leaf]` and then forwarding the sub-path to that topic's `message_received`.
|
|
70
|
+
|
|
71
|
+
**Existing `MqttTopic.groups` getter** (lines 812–820, derives groups from flat topics) is removed; callers use `MqttTopicNode.groups` directly.
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
### 3. Move MQTT message routing from `MqttNode` to `MqttTopicNode`
|
|
76
|
+
|
|
77
|
+
**This is the largest change.**
|
|
78
|
+
|
|
79
|
+
`MqttNode.topicValueSet` (lines 3004–3090) strips the node prefix and `set/`, applies legacy-topic filtering, auto-creates groups from templates, and dispatches to `sendMessageToMatchingTopics`. This is data-tree logic, not UX logic.
|
|
80
|
+
|
|
81
|
+
**New structure:**
|
|
82
|
+
|
|
83
|
+
- `MqttTopicNode.topicValueSet(topicPath, message)` carries the full routing and group-creation logic. Where the existing code performs UX-specific actions (appending elements to the DOM, triggering re-renders, rebuilding dropdowns), it calls a method on `this.element` (the paired `MqttNode`), guarded by `if (this.element)`.
|
|
84
|
+
- `MqttNode.topicValueSet` becomes a thin override that calls `super` and handles only its UX-specific additions.
|
|
85
|
+
- `MqttTopicProject.topicValueSet` mirrors the same split for the project level.
|
|
86
|
+
- `MqttTopic.message_received` calls `this.node.topicValueSet(...)` directly, targeting the `MqttTopicNode`.
|
|
87
|
+
|
|
88
|
+
The legacy-topic filter (the long chain of `twig.startsWith` / `twig.endsWith` guards) stays in `MqttTopicNode.topicValueSet` — it is data routing logic, not UX.
|
|
89
|
+
|
|
90
|
+
Group auto-creation (`addGroupFromTemplate`) moves to `MqttTopicNode` and produces a `MqttTopicGroup`. If an element is present, the element's equivalent is then created and linked.
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
### 4. Move leaf and group state from element `state` to data-tree objects
|
|
95
|
+
|
|
96
|
+
**No parallel copies — state moves, not duplicates.**
|
|
97
|
+
|
|
98
|
+
State that describes the observed data (rather than UX presentation) moves from element `this.state` to the corresponding data-tree object:
|
|
99
|
+
|
|
100
|
+
- Leaf state (`value`, `wired`, `min`, `max`, `color`, `type`, `rw`, …) moves to `MqttTopic.state`.
|
|
101
|
+
- Group state (`name`, `on`, `now_wired`, `out_wired`, `limit_wired`, …) moves to `MqttTopicGroup.state`.
|
|
102
|
+
|
|
103
|
+
Names are preserved: `element.state.wired` becomes `topic.state.wired`, `element.state.on` becomes `groupTopic.state.on`, etc. UX elements read these fields from their associated topic via `this.mt.state.*`.
|
|
104
|
+
|
|
105
|
+
The `this.data` array on `MqttTopic` (time-series history for graphing) is not affected.
|
|
106
|
+
|
|
107
|
+
**Roll-up (leaf → group):** Currently `MqttReceiver.topicValueSet` calls `groupElement.setAttribute('now_wired', value)` (lines 2067–2078). This becomes: write to `this.mt.groupTopic.state.now_wired = value` (the `MqttTopicGroup` for this leaf's group). If a group element exists it is notified separately. This is also what enables the `frugaliot:groupchanged` event (change 7).
|
|
108
|
+
|
|
109
|
+
**`MqttTopic.groupName` / `usableName`:** currently reads `groupEl.state.name`. After this change it reads `this.groupMt.state.name` (the `MqttTopicGroup`) — no element traversal.
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
### 5. `MqttProject.state.nodes` goes away
|
|
114
|
+
|
|
115
|
+
`this.mt.nodes[id]` (keyed nodeId → `MqttTopicNode`) is the authoritative index; it already exists (line 2856). `MqttProject.state.nodes[id]` (which stores the element) is removed. UX code that currently reads `state.nodes` is updated to use `this.mt.nodes` and reach the element via `nodeMt.element` where needed.
|
|
116
|
+
|
|
117
|
+
`findTopic` (updated in change 2) navigates through `this.mt.nodes` and the grouped structure.
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
### 6. `wiredTopic` — replace DOM traversal with a direct data reference
|
|
122
|
+
|
|
123
|
+
Current: `this.project` → `this.node.project` → `this.node.closest("mqtt-project")` — DOM traversal.
|
|
124
|
+
|
|
125
|
+
New: `fromTemplate` (and the equivalent initialisers on `MqttTopicNode` / `MqttTopicGroup`) stores a direct reference to the `MqttTopicProject` as `this.projectMt`. `wiredTopic` becomes `this.projectMt.findTopic(this.wired)` — no DOM required. Also a correctness improvement for temporarily detached elements.
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
### 7. Replace the element reference in `frugaliot:topicschanged`
|
|
130
|
+
|
|
131
|
+
Current: `{ project: this }` where `this` is the `MqttProject` element.
|
|
132
|
+
|
|
133
|
+
New: `{ project: this.mt }` — the `MqttTopicProject`. Dashboards that already access `wrapper.projectMt` need no change.
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
### 8. Replace `MutationObserver` with a `frugaliot:groupchanged` event
|
|
138
|
+
|
|
139
|
+
`dashboard_example2.html` uses a `MutationObserver` on the group element to detect `now_wired`, `out_wired`, and `on` attribute changes, then re-runs `wireUpDashboard`.
|
|
140
|
+
|
|
141
|
+
Once group state lives on `MqttTopicGroup` (change 4), `MqttTopicNode.topicValueSet` fires a `frugaliot:groupchanged` CustomEvent whenever a group-level state field changes. The event carries `{ nodeMt, groupId, changed: ['now_wired'] }`.
|
|
142
|
+
|
|
143
|
+
Dashboards subscribe to this event instead of observing DOM mutations. This works in headless mode and removes the dependency on the group element being in the DOM.
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## What Does NOT Need to Change
|
|
148
|
+
|
|
149
|
+
- `MqttTopic.message_received` already has a headless path: if `this.element` is absent it stores into `this.data[]`. After change 4 it also updates `this.state.value`. No restructuring of this method is needed.
|
|
150
|
+
- `setWired` / `publishWired` work on `MqttTopic` directly — no element required.
|
|
151
|
+
- `frugaliot:controlgroup` already fires with data-tree references only — no change needed.
|
|
152
|
+
- `mqtt-choosetopic` still needs a DOM element to render a `<select>`, but it reads options from the data tree.
|
|
153
|
+
- The graph (`MqttGraph`, `MqttGraphDataset`) operates on `MqttTopic` directly — no change required.
|
|
154
|
+
- `MqttTopic.createElement()` continues to work in headless mode — dashboards can call it on any leaf topic to get a standalone renderable element.
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## Sequencing (suggested)
|
|
159
|
+
|
|
160
|
+
1. **5** (`MqttProject.state.nodes` → `this.mt.nodes`) — isolated, removes a redundant map
|
|
161
|
+
2. **6** (`this.project` via direct reference) — isolated, correctness improvement
|
|
162
|
+
3. **7** (`frugaliot:topicschanged` payload) — small, any time
|
|
163
|
+
4. **2** (introduce `MqttTopicGroup`, restructure node topics into groups) — enables cleaner state migration
|
|
164
|
+
5. **4** (state moves from element to data-tree objects) — depends on 2 for group state
|
|
165
|
+
6. **3** (introduce `MqttTopicNode`/`MqttTopicProject`, move routing) — largest change; depends on 2 + 4
|
|
166
|
+
7. **8** (replace `MutationObserver` with `frugaliot:groupchanged`) — depends on 3 + 4
|
|
167
|
+
8. **1** (add `headless` attribute) — gates actual UI suppression; can be added once 3–5 are done
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Mitra Ardron
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Frugal IoT client
|
|
2
|
+
|
|
3
|
+
This is a javascript based client for the Frugal IoT project, in particular it has the
|
|
4
|
+
following features
|
|
5
|
+
|
|
6
|
+
* Can connect to MQTT.
|
|
7
|
+
* Understands the discovery messages of Frugal IoT so can determine what sensors are available
|
|
8
|
+
* Can display results from sensors in Bar graphs, toggles, with other UX coming soon
|
|
9
|
+
* Can control nodes including both direct control
|
|
10
|
+
e.g. toggling a relay - and configuting various parameters.
|
|
11
|
+
* Can show graph results for multiple readings
|
|
12
|
+
|
|
13
|
+
There is much still to do - see https://github.com/mitra42/frugal-iot-client.
|
|
14
|
+
This repo was spun out of the main repo https://github.com/mitra42/frugal-iot
|
|
15
|
+
which is still the best place to post complex issues involving client / server and node.
|
|
16
|
+
|
|
17
|
+
## Test and demonstration
|
|
18
|
+
|
|
19
|
+
Connect to https://frugaliot.naturalinnovation it will server up the current UI.
|
|
20
|
+
|
|
21
|
+
## Installation for development
|
|
22
|
+
|
|
23
|
+
Note this process will probably change to make configuration changes for development easier.
|
|
24
|
+
|
|
25
|
+
This should work on Linux or a Mac - (instructions on windows welcome as a PR)
|
|
26
|
+
Clone this repo,
|
|
27
|
+
```
|
|
28
|
+
git clone https://github.com/mitra42/frugal-iot-client.git
|
|
29
|
+
cd frugal-iot-client; npm install
|
|
30
|
+
cd ..
|
|
31
|
+
```
|
|
32
|
+
Clone and run the server
|
|
33
|
+
```
|
|
34
|
+
git clone https://github.com/mitra42/frugal-iot-server.git
|
|
35
|
+
cd frugal-iot-server; npm install
|
|
36
|
+
```
|
|
37
|
+
Edit `frugal-iot-server.js` and uncomment the line hear the top
|
|
38
|
+
that sets `htmldir` to the `../frugal-iot-client`
|
|
39
|
+
```
|
|
40
|
+
node frugal-iot-server.js
|
|
41
|
+
```
|
|
42
|
+
Point your browser at `https://localhost:8080`
|
|
43
|
+
|
|
44
|
+
You should see a series of dialogues allowing you to select a project.
|
|
45
|
+
Choosing `dev` and `lotus` will often get one of the test nodes I'm running at home.
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
|