iobroker.mywebui 1.42.14 → 1.42.16

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/io-package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "mywebui",
4
- "version": "1.42.14",
4
+ "version": "1.42.16",
5
5
  "titleLang": {
6
6
  "en": "mywebui",
7
7
  "de": "mywebui",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iobroker.mywebui",
3
- "version": "1.42.14",
3
+ "version": "1.42.16",
4
4
  "description": "ioBroker mywebui - Custom edited mywebui by gokturk413 with 3D Editor",
5
5
  "type": "module",
6
6
  "main": "dist/backend/main.js",
@@ -0,0 +1,565 @@
1
+ /**
2
+ * 3D Drive Engine for mywebui 3D Screen Editor
3
+ *
4
+ * Kinematics system inspired by realvirtual's architecture.
5
+ * Written from scratch - no code copied, same logic independently implemented.
6
+ *
7
+ * Concepts:
8
+ * Drive - moves a Three.js Object3D along an axis (linear mm or rotation deg)
9
+ * SignalBus - subscribes to ioBroker signals via iobrokerHandler
10
+ * DriveCylinder - extends/retracts between MinPos/MaxPos on ioBroker bool signals
11
+ * DriveSimple - jogs continuously Forward/Backward on ioBroker bool signals
12
+ */
13
+
14
+ import { iobrokerHandler } from '../common/IobrokerHandler.js';
15
+
16
+ // ── Constants ────────────────────────────────────────────────────────────────
17
+
18
+ const MM_TO_M = 0.001;
19
+ const DEG_TO_RAD = Math.PI / 180;
20
+
21
+ // ── Drive directions ─────────────────────────────────────────────────────────
22
+
23
+ export const DriveDirection = {
24
+ LinearX: 'LinearX',
25
+ LinearY: 'LinearY',
26
+ LinearZ: 'LinearZ',
27
+ RotationX: 'RotationX',
28
+ RotationY: 'RotationY',
29
+ RotationZ: 'RotationZ',
30
+ };
31
+
32
+ function isRotation(dir) {
33
+ return dir === DriveDirection.RotationX ||
34
+ dir === DriveDirection.RotationY ||
35
+ dir === DriveDirection.RotationZ;
36
+ }
37
+
38
+ // ── Signal Bus ───────────────────────────────────────────────────────────────
39
+
40
+ /**
41
+ * SignalBus - bridges ioBroker states to the drive engine.
42
+ * Subscribes to ioBroker signals and dispatches value changes to listeners.
43
+ * ioBrokerHandler.subscribeState is used for live updates.
44
+ */
45
+ export class SignalBus {
46
+ constructor() {
47
+ /** signalPath → current value */
48
+ this._values = new Map();
49
+ /** signalPath → Set<callback> */
50
+ this._listeners = new Map();
51
+ /** signalPath → unsubscribe function */
52
+ this._unsubs = new Map();
53
+ }
54
+
55
+ /** Subscribe to an ioBroker signal. Returns unsubscribe function. */
56
+ subscribe(signalPath, cb) {
57
+ if (!signalPath) return () => {};
58
+
59
+ // Register listener
60
+ if (!this._listeners.has(signalPath)) {
61
+ this._listeners.set(signalPath, new Set());
62
+ }
63
+ this._listeners.get(signalPath).add(cb);
64
+
65
+ // Subscribe to ioBroker if not already
66
+ if (!this._unsubs.has(signalPath)) {
67
+ const unsub = iobrokerHandler.subscribe(signalPath, (id, state) => {
68
+ const val = state?.val ?? false;
69
+ this._values.set(signalPath, val);
70
+ const subs = this._listeners.get(signalPath);
71
+ if (subs) for (const fn of subs) fn(val);
72
+ });
73
+ this._unsubs.set(signalPath, unsub);
74
+ }
75
+
76
+ // Fire immediately with current value if known
77
+ if (this._values.has(signalPath)) {
78
+ cb(this._values.get(signalPath));
79
+ }
80
+
81
+ return () => {
82
+ const subs = this._listeners.get(signalPath);
83
+ if (subs) {
84
+ subs.delete(cb);
85
+ if (subs.size === 0) {
86
+ this._listeners.delete(signalPath);
87
+ const unsub = this._unsubs.get(signalPath);
88
+ if (unsub) unsub();
89
+ this._unsubs.delete(signalPath);
90
+ }
91
+ }
92
+ };
93
+ }
94
+
95
+ /** Get current value of a signal (or undefined) */
96
+ get(signalPath) {
97
+ return this._values.get(signalPath);
98
+ }
99
+
100
+ /** Write a value back to ioBroker (for feedback signals) */
101
+ set(signalPath, value) {
102
+ if (!signalPath) return;
103
+ iobrokerHandler.setState(signalPath, value);
104
+ }
105
+
106
+ /** Dispose all subscriptions */
107
+ dispose() {
108
+ for (const unsub of this._unsubs.values()) {
109
+ try { unsub(); } catch (_) {}
110
+ }
111
+ this._values.clear();
112
+ this._listeners.clear();
113
+ this._unsubs.clear();
114
+ }
115
+ }
116
+
117
+ // ── Drive ────────────────────────────────────────────────────────────────────
118
+
119
+ /**
120
+ * Drive - moves a Three.js Object3D along a single axis.
121
+ *
122
+ * Position units: mm for linear, degrees for rotation.
123
+ * Three.js units: meters for linear, radians for rotation.
124
+ */
125
+ export class Drive {
126
+ /**
127
+ * @param {Object3D} node - Three.js object to animate
128
+ * @param {object} config - drive configuration from asset.drives[]
129
+ */
130
+ constructor(node, config = {}) {
131
+ this.node = node;
132
+ this.name = node.name || 'Drive';
133
+
134
+ // Config (from asset JSON)
135
+ this.direction = config.direction ?? DriveDirection.LinearX;
136
+ this.reverseDir = config.reverseDir ?? false;
137
+ this.offset = config.offset ?? 0;
138
+ this.startPosition = config.startPosition ?? 0;
139
+ this.targetSpeed = config.targetSpeed ?? 100; // mm/s or deg/s
140
+ this.acceleration = config.acceleration ?? 0; // 0 = instant
141
+ this.useLimits = config.useLimits ?? false;
142
+ this.lowerLimit = config.lowerLimit ?? -180;
143
+ this.upperLimit = config.upperLimit ?? 180;
144
+
145
+ // Runtime state
146
+ this.currentPosition = this.startPosition;
147
+ this.targetPosition = this.startPosition;
148
+ this.currentSpeed = 0;
149
+ this.isRunning = false;
150
+ this.jogForward = false;
151
+ this.jogBackward = false;
152
+
153
+ // Base transform (rest position)
154
+ this._basePos = node.position.clone();
155
+ this._baseQuat = node.quaternion.clone();
156
+
157
+ this._isRotation = isRotation(this.direction);
158
+
159
+ // Callbacks
160
+ this.onAfterUpdate = null; // (drive) => void
161
+
162
+ // Apply start position immediately
163
+ this._applyToNode();
164
+ }
165
+
166
+ get isAtTarget() {
167
+ return Math.abs(this.currentPosition - this.targetPosition) < 0.01;
168
+ }
169
+
170
+ /** Move to a specific position */
171
+ moveTo(pos, speed) {
172
+ this.targetPosition = pos;
173
+ if (speed !== undefined) this.targetSpeed = speed;
174
+ this.isRunning = true;
175
+ }
176
+
177
+ stop() {
178
+ this.isRunning = false;
179
+ this.currentSpeed = 0;
180
+ }
181
+
182
+ /** Called every frame from DriveEngine tick */
183
+ update(dt) {
184
+ if (this.jogForward || this.jogBackward) {
185
+ // Jog mode: continuous motion (conveyor-style)
186
+ this.currentSpeed = this.targetSpeed;
187
+ const dir = this.jogForward ? 1 : -1;
188
+ let next = this.currentPosition + dir * this.currentSpeed * dt;
189
+ if (this.useLimits) {
190
+ next = Math.max(this.lowerLimit, Math.min(this.upperLimit, next));
191
+ }
192
+ this.currentPosition = next;
193
+ this._applyToNode();
194
+ this.onAfterUpdate?.(this);
195
+ return;
196
+ }
197
+
198
+ if (!this.isRunning) return;
199
+
200
+ const dist = this.targetPosition - this.currentPosition;
201
+ if (Math.abs(dist) < 0.01) {
202
+ this.currentPosition = this.targetPosition;
203
+ this.isRunning = false;
204
+ this.currentSpeed = 0;
205
+ this._applyToNode();
206
+ this.onAfterUpdate?.(this);
207
+ return;
208
+ }
209
+
210
+ const dir = Math.sign(dist);
211
+ const spd = this.targetSpeed;
212
+
213
+ if (this.acceleration > 0) {
214
+ // Kinematic acceleration/deceleration
215
+ const stopDist = (this.currentSpeed * this.currentSpeed) / (2 * this.acceleration);
216
+ if (stopDist >= Math.abs(dist)) {
217
+ this.currentSpeed = Math.max(0, this.currentSpeed - this.acceleration * dt);
218
+ } else if (this.currentSpeed < spd) {
219
+ this.currentSpeed = Math.min(spd, this.currentSpeed + this.acceleration * dt);
220
+ }
221
+ } else {
222
+ this.currentSpeed = spd;
223
+ }
224
+
225
+ let next = this.currentPosition + dir * this.currentSpeed * dt;
226
+ // Clamp to target
227
+ if (dir > 0 && next > this.targetPosition) next = this.targetPosition;
228
+ if (dir < 0 && next < this.targetPosition) next = this.targetPosition;
229
+ // Apply limits
230
+ if (this.useLimits) {
231
+ next = Math.max(this.lowerLimit, Math.min(this.upperLimit, next));
232
+ }
233
+
234
+ this.currentPosition = next;
235
+ this._applyToNode();
236
+ this.onAfterUpdate?.(this);
237
+ }
238
+
239
+ _applyToNode() {
240
+ const pos = this.currentPosition + this.offset;
241
+ const sign = this.reverseDir ? -1 : 1;
242
+
243
+ switch (this.direction) {
244
+ case DriveDirection.LinearX:
245
+ this.node.position.x = this._basePos.x + sign * pos * MM_TO_M;
246
+ break;
247
+ case DriveDirection.LinearY:
248
+ this.node.position.y = this._basePos.y + sign * pos * MM_TO_M;
249
+ break;
250
+ case DriveDirection.LinearZ:
251
+ this.node.position.z = this._basePos.z + sign * pos * MM_TO_M;
252
+ break;
253
+ case DriveDirection.RotationX: {
254
+ const rad = sign * pos * DEG_TO_RAD;
255
+ this.node.quaternion.copy(this._baseQuat);
256
+ this.node.rotateX(rad);
257
+ break;
258
+ }
259
+ case DriveDirection.RotationY: {
260
+ const rad = sign * pos * DEG_TO_RAD;
261
+ this.node.quaternion.copy(this._baseQuat);
262
+ this.node.rotateY(rad);
263
+ break;
264
+ }
265
+ case DriveDirection.RotationZ: {
266
+ const rad = sign * pos * DEG_TO_RAD;
267
+ this.node.quaternion.copy(this._baseQuat);
268
+ this.node.rotateZ(rad);
269
+ break;
270
+ }
271
+ }
272
+ }
273
+
274
+ /** Serialize current config back to JSON */
275
+ toConfig() {
276
+ return {
277
+ direction: this.direction,
278
+ reverseDir: this.reverseDir,
279
+ offset: this.offset,
280
+ startPosition: this.startPosition,
281
+ targetSpeed: this.targetSpeed,
282
+ acceleration: this.acceleration,
283
+ useLimits: this.useLimits,
284
+ lowerLimit: this.lowerLimit,
285
+ upperLimit: this.upperLimit,
286
+ };
287
+ }
288
+ }
289
+
290
+ // ── DriveCylinder ─────────────────────────────────────────────────────────────
291
+
292
+ /**
293
+ * DriveCylinder - pneumatic/hydraulic cylinder automation.
294
+ *
295
+ * Listens to two ioBroker signals (Out/In).
296
+ * Out=true → moves to MaxPos at stroke/TimeOut speed
297
+ * In=true → moves to MinPos at stroke/TimeIn speed
298
+ * Writes feedback signals: IsOut, IsIn, IsMovingOut, IsMovingIn
299
+ */
300
+ export class DriveCylinder {
301
+ /**
302
+ * @param {Drive} drive - the underlying drive
303
+ * @param {SignalBus} bus - signal bus
304
+ * @param {object} config - cylinder config
305
+ */
306
+ constructor(drive, bus, config = {}) {
307
+ this.drive = drive;
308
+ this.bus = bus;
309
+ this.name = drive.name;
310
+
311
+ this.minPos = config.minPos ?? 0;
312
+ this.maxPos = config.maxPos ?? 100;
313
+ this.timeOut = config.timeOut ?? 1; // seconds to extend
314
+ this.timeIn = config.timeIn ?? 1; // seconds to retract
315
+ this.oneBit = config.oneBit ?? false;
316
+ this.invertLogic = config.invertLogic ?? false;
317
+
318
+ // ioBroker signal paths
319
+ this.signalOut = config.signalOut ?? null;
320
+ this.signalIn = config.signalIn ?? null;
321
+ this.signalIsOut = config.signalIsOut ?? null;
322
+ this.signalIsIn = config.signalIsIn ?? null;
323
+ this.signalMovingOut = config.signalMovingOut ?? null;
324
+ this.signalMovingIn = config.signalMovingIn ?? null;
325
+
326
+ this._unsubs = [];
327
+ this._init();
328
+ }
329
+
330
+ _init() {
331
+ const { drive, bus } = this;
332
+ const stroke = Math.abs(this.maxPos - this.minPos);
333
+
334
+ // Set initial position
335
+ drive.currentPosition = this.minPos;
336
+ drive._applyToNode();
337
+
338
+ if (this.signalOut) {
339
+ const unsub = bus.subscribe(this.signalOut, (val) => {
340
+ let v = Boolean(val);
341
+ if (this.invertLogic) v = !v;
342
+ if (this.oneBit) {
343
+ drive.targetSpeed = stroke / (v ? this.timeOut : this.timeIn);
344
+ drive.moveTo(v ? this.maxPos : this.minPos);
345
+ } else if (v) {
346
+ drive.targetSpeed = stroke / this.timeOut;
347
+ drive.moveTo(this.maxPos);
348
+ }
349
+ });
350
+ this._unsubs.push(unsub);
351
+ }
352
+
353
+ if (this.signalIn && !this.oneBit) {
354
+ const unsub = bus.subscribe(this.signalIn, (val) => {
355
+ let v = Boolean(val);
356
+ if (this.invertLogic) v = !v;
357
+ if (v) {
358
+ drive.targetSpeed = stroke / this.timeIn;
359
+ drive.moveTo(this.minPos);
360
+ }
361
+ });
362
+ this._unsubs.push(unsub);
363
+ }
364
+
365
+ // Feedback signals via onAfterUpdate
366
+ if (this.signalIsOut || this.signalIsIn || this.signalMovingOut || this.signalMovingIn) {
367
+ let prevIsOut = false, prevIsIn = true;
368
+ drive.onAfterUpdate = (d) => {
369
+ const atMax = Math.abs(d.currentPosition - this.maxPos) < 0.01;
370
+ const atMin = Math.abs(d.currentPosition - this.minPos) < 0.01;
371
+ const movingOut = d.isRunning && d.targetPosition === this.maxPos;
372
+ const movingIn = d.isRunning && d.targetPosition === this.minPos;
373
+
374
+ if (atMax !== prevIsOut) {
375
+ if (this.signalIsOut) bus.set(this.signalIsOut, atMax);
376
+ if (this.signalIsOut) bus.set(this.signalIsOut + '_max', atMax);
377
+ prevIsOut = atMax;
378
+ }
379
+ if (atMin !== prevIsIn) {
380
+ if (this.signalIsIn) bus.set(this.signalIsIn, atMin);
381
+ prevIsIn = atMin;
382
+ }
383
+ if (this.signalMovingOut) bus.set(this.signalMovingOut, movingOut);
384
+ if (this.signalMovingIn) bus.set(this.signalMovingIn, movingIn);
385
+ };
386
+ }
387
+ }
388
+
389
+ dispose() {
390
+ for (const fn of this._unsubs) fn();
391
+ this._unsubs = [];
392
+ }
393
+
394
+ toConfig() {
395
+ return {
396
+ type: 'cylinder',
397
+ minPos: this.minPos,
398
+ maxPos: this.maxPos,
399
+ timeOut: this.timeOut,
400
+ timeIn: this.timeIn,
401
+ oneBit: this.oneBit,
402
+ invertLogic: this.invertLogic,
403
+ signalOut: this.signalOut,
404
+ signalIn: this.signalIn,
405
+ signalIsOut: this.signalIsOut,
406
+ signalIsIn: this.signalIsIn,
407
+ signalMovingOut: this.signalMovingOut,
408
+ signalMovingIn: this.signalMovingIn,
409
+ };
410
+ }
411
+ }
412
+
413
+ // ── DriveSimple ───────────────────────────────────────────────────────────────
414
+
415
+ /**
416
+ * DriveSimple - continuous jog motion (conveyor belt, rotating drum).
417
+ * Forward=true → jog forward, Backward=true → jog backward
418
+ */
419
+ export class DriveSimple {
420
+ constructor(drive, bus, config = {}) {
421
+ this.drive = drive;
422
+ this.bus = bus;
423
+ this.signalForward = config.signalForward ?? null;
424
+ this.signalBackward = config.signalBackward ?? null;
425
+ this._unsubs = [];
426
+ this._init();
427
+ }
428
+
429
+ _init() {
430
+ const { drive, bus } = this;
431
+ if (this.signalForward) {
432
+ const unsub = bus.subscribe(this.signalForward, (v) => {
433
+ drive.jogForward = Boolean(v);
434
+ if (drive.jogForward) drive.jogBackward = false;
435
+ });
436
+ this._unsubs.push(unsub);
437
+ }
438
+ if (this.signalBackward) {
439
+ const unsub = bus.subscribe(this.signalBackward, (v) => {
440
+ drive.jogBackward = Boolean(v);
441
+ if (drive.jogBackward) drive.jogForward = false;
442
+ });
443
+ this._unsubs.push(unsub);
444
+ }
445
+ }
446
+
447
+ dispose() {
448
+ for (const fn of this._unsubs) fn();
449
+ this._unsubs = [];
450
+ }
451
+
452
+ toConfig() {
453
+ return {
454
+ type: 'simple',
455
+ signalForward: this.signalForward,
456
+ signalBackward: this.signalBackward,
457
+ };
458
+ }
459
+ }
460
+
461
+ // ── DriveEngine ───────────────────────────────────────────────────────────────
462
+
463
+ /**
464
+ * DriveEngine - main animation/kinematics engine.
465
+ *
466
+ * Manages all drives and ticks them on each animation frame.
467
+ * Integrates with the 3D editor's animation loop.
468
+ *
469
+ * Usage:
470
+ * const engine = new DriveEngine();
471
+ * engine.createFromAssets(sceneData.assets, threeObjects);
472
+ * // In animation loop:
473
+ * engine.tick(dt);
474
+ */
475
+ export class DriveEngine {
476
+ constructor() {
477
+ this.bus = new SignalBus();
478
+ this.drives = []; // Drive[]
479
+ this.controllers = []; // DriveCylinder | DriveSimple
480
+ this._lastTime = null;
481
+ }
482
+
483
+ /**
484
+ * Create drives from sceneData assets.
485
+ * Each asset can have an array `drives` in its config:
486
+ *
487
+ * asset.drives = [{
488
+ * direction: 'LinearY',
489
+ * targetSpeed: 50,
490
+ * type: 'cylinder',
491
+ * minPos: 0, maxPos: 200,
492
+ * timeOut: 1.5, timeIn: 1.0,
493
+ * signalOut: 'mywebui.0.device.cylinder_out',
494
+ * signalIn: 'mywebui.0.device.cylinder_in',
495
+ * signalIsOut: 'mywebui.0.device.cylinder_isout',
496
+ * }]
497
+ */
498
+ createFromAssets(assets, threeObjects) {
499
+ this.dispose();
500
+
501
+ for (const asset of assets) {
502
+ if (!asset.drives || asset.drives.length === 0) continue;
503
+
504
+ // Find matching Three.js object
505
+ const obj = threeObjects.find(o => o.userData.assetData?.id === asset.id);
506
+ if (!obj) continue;
507
+
508
+ for (const driveCfg of asset.drives) {
509
+ const drive = new Drive(obj, driveCfg);
510
+ this.drives.push(drive);
511
+
512
+ if (driveCfg.type === 'cylinder') {
513
+ const ctrl = new DriveCylinder(drive, this.bus, driveCfg);
514
+ this.controllers.push(ctrl);
515
+ } else if (driveCfg.type === 'simple') {
516
+ const ctrl = new DriveSimple(drive, this.bus, driveCfg);
517
+ this.controllers.push(ctrl);
518
+ }
519
+ }
520
+ }
521
+ }
522
+
523
+ /**
524
+ * Tick all drives. Call this in the Three.js animation loop.
525
+ * @param {number} dt - delta time in seconds
526
+ */
527
+ tick(dt) {
528
+ for (const drive of this.drives) {
529
+ drive.update(dt);
530
+ }
531
+ }
532
+
533
+ /** Called from Three.js requestAnimationFrame - auto-computes dt */
534
+ tickFromRAF(timestamp) {
535
+ if (this._lastTime === null) { this._lastTime = timestamp; return; }
536
+ const dt = Math.min((timestamp - this._lastTime) / 1000, 0.1); // cap at 100ms
537
+ this._lastTime = timestamp;
538
+ this.tick(dt);
539
+ }
540
+
541
+ /** Dispose all subscriptions and clear drives */
542
+ dispose() {
543
+ for (const ctrl of this.controllers) ctrl.dispose();
544
+ this.controllers = [];
545
+ this.drives = [];
546
+ }
547
+
548
+ /** Full dispose including signal bus */
549
+ destroy() {
550
+ this.dispose();
551
+ this.bus.dispose();
552
+ }
553
+
554
+ /** Get drive by asset id and drive index */
555
+ getDrive(assetId, driveIndex = 0) {
556
+ let count = 0;
557
+ for (const drive of this.drives) {
558
+ if (drive.node.userData.assetData?.id === assetId) {
559
+ if (count === driveIndex) return drive;
560
+ count++;
561
+ }
562
+ }
563
+ return null;
564
+ }
565
+ }