homebridge-smartsystem 6.8.13 → 7.0.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/.vscode/launch.json +15 -4
- package/MIXINS-GUIDE.md +210 -0
- package/README.md +32 -0
- package/SETTINGS-UPDATE.md +195 -0
- package/TESTING-GUIDE.md +220 -0
- package/WEBSOCKET-API.md +303 -0
- package/accessories/accessory.js +20 -4
- package/accessories/accessory.ts +19 -4
- package/accessories/bulb.js +0 -5
- package/accessories/bulb.ts +1 -5
- package/accessories/dimmer.js +11 -3
- package/accessories/dimmer.ts +11 -4
- package/accessories/garagedoor.js +19 -9
- package/accessories/garagedoor.ts +21 -9
- package/accessories/lock.js +11 -3
- package/accessories/lock.ts +10 -3
- package/accessories/temperature.js +10 -2
- package/accessories/temperature.ts +9 -2
- package/accessories/windowcovering.js +30 -14
- package/accessories/windowcovering.ts +29 -15
- package/config.json +57 -19
- package/duotecno/types.js +48 -2
- package/duotecno/types.ts +75 -1
- package/package.json +12 -4
- package/raspberry-pi-setup.sh +80 -0
- package/server/HA-API.js +4 -1
- package/server/HA-API.ts +4 -1
- package/server/HB.js +6 -1
- package/server/HB.ts +5 -1
- package/server/mDNS.js +168 -0
- package/server/mDNS.ts +198 -0
- package/server/platform.js +7 -0
- package/server/platform.ts +8 -0
- package/server/proxy.js +58 -19
- package/server/proxy.ts +31 -3
- package/server/smartapp.js +397 -18
- package/server/smartapp.ts +432 -20
- package/server/socapp.js +2 -1
- package/server/socapp.ts +2 -1
- package/server/views/assets/Duotecno_logo_black.svg +10 -0
- package/server/views/assets/Duotecno_logo_white.svg +10 -0
- package/server/views/device-details.ejs +139 -0
- package/server/views/device-list.ejs +44 -0
- package/server/views/footer.ejs +0 -1
- package/server/views/head.ejs +9 -0
- package/server/views/homekit.ejs +2 -9
- package/server/views/link-details.ejs +2 -15
- package/server/views/link-list.ejs +2 -9
- package/server/views/login.ejs +8 -9
- package/server/views/master-detail.ejs +3 -9
- package/server/views/master-list.ejs +3 -10
- package/server/views/nav.ejs +44 -11
- package/server/views/node-details.ejs +1 -8
- package/server/views/power-binding.ejs +3 -10
- package/server/views/power-rule.ejs +3 -10
- package/server/views/power.ejs +8 -15
- package/server/views/proxy.ejs +1 -8
- package/server/views/scripts.ejs +10 -0
- package/server/views/service-list.ejs +2 -9
- package/server/views/settings.ejs +61 -22
- package/server/views/style.ejs +49 -5
- package/server/views/switch-details.ejs +2 -9
- package/server/views/switch-list.ejs +2 -9
- package/server/webapp.js +150 -11
- package/server/webapp.ts +166 -14
- package/{index.spec.js → testHB.js} +1 -1
- package/{run.js → testProxy.js} +2 -2
- package/{run.ts → testProxy.ts} +1 -1
- package/testWS.js +137 -0
- package/testWS.ts +174 -0
- package/run9998.js +0 -7
- package/run9998.ts +0 -6
- package/run9999.js +0 -7
- package/run9999.ts +0 -6
- /package/{index.spec.ts → testHB.ts} +0 -0
package/server/smartapp.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// v2.0: mar/apr 2020
|
|
10
10
|
|
|
11
11
|
import { Context, HttpResponse, WebApp } from "./webapp";
|
|
12
|
-
import { Sanitizers, Rule, kEmptyRule, actionValue, Action, kEmptySwitch, Switch, SwitchType, hex, Binding, NetworkConfig, SettingsConfig, kEmptySettings, Link, LinkType, kEmptyLink } from "../duotecno/types";
|
|
12
|
+
import { Sanitizers, Rule, kEmptyRule, actionValue, Action, kEmptySwitch, Switch, SwitchType, hex, Binding, NetworkConfig, SettingsConfig, kEmptySettings, Link, LinkType, kEmptyLink, Device, kEmptyDevice, PowerSource } from "../duotecno/types";
|
|
13
13
|
import { log, debug, err } from "../duotecno/logger";
|
|
14
14
|
import { System } from "../duotecno/system";
|
|
15
15
|
import { Node, Unit } from "../duotecno/protocol";
|
|
@@ -25,6 +25,7 @@ import { PowerBase } from "./powerbase";
|
|
|
25
25
|
import { dt2hbvalue, hb2dtvalue, hbAccessories, hbGetValue, hbLogin, hbName, hbService, hbSetState, hbStop, hbSubscribe } from "./HB";
|
|
26
26
|
import { httpRequest, ContentType } from "./HA-API";
|
|
27
27
|
import { cleanStart, kEmptyProxy, ProxyConfig, setProxyConfig } from "./proxy";
|
|
28
|
+
import * as WebSocket from 'ws';
|
|
28
29
|
|
|
29
30
|
const kMaster = {name: "master", type: "string", default: "0.0.0.0:5001"} as const;
|
|
30
31
|
const kAddress = {name: "address", type: "string", default: "0.0.0.0"} as const;
|
|
@@ -60,7 +61,9 @@ export class SmartApp extends WebApp {
|
|
|
60
61
|
platform: Platform;
|
|
61
62
|
switches: Array<Switch>;
|
|
62
63
|
links: Array<Link>;
|
|
64
|
+
devices: Array<Device>;
|
|
63
65
|
proxy: ProxyConfig;
|
|
66
|
+
wsServers: WebSocket.Server[]; // WebSocket servers for device control
|
|
64
67
|
|
|
65
68
|
|
|
66
69
|
constructor(system: System, power: Power, platform: Platform) {
|
|
@@ -73,9 +76,31 @@ export class SmartApp extends WebApp {
|
|
|
73
76
|
this.system.emitter.on('plug', this.alertSwitch.bind(this));
|
|
74
77
|
|
|
75
78
|
// get some configurated params
|
|
76
|
-
|
|
79
|
+
const configuredPort = this.config.port || 80;
|
|
77
80
|
this.password = platform.config.password || this.config.password || "";
|
|
78
|
-
this.user = platform.config.user || this.config.user || "pi";
|
|
81
|
+
this.user = platform.config.user || this.config.user || "pi";
|
|
82
|
+
|
|
83
|
+
// Set up multi-port listening if port is not 80
|
|
84
|
+
// Note: Port 80 requires root/sudo on Unix-like systems
|
|
85
|
+
// For development on macOS/Linux without sudo, use only configured port
|
|
86
|
+
if (configuredPort !== 80) {
|
|
87
|
+
// Check if we're running with appropriate privileges (process.getuid exists only on Unix)
|
|
88
|
+
const canBindPort80 = process.platform === 'win32' || (process.getuid && process.getuid() === 0);
|
|
89
|
+
|
|
90
|
+
if (canBindPort80) {
|
|
91
|
+
log("smartapp", `Configured for dual-port mode: ${configuredPort} and 80`);
|
|
92
|
+
this.port = [80, configuredPort];
|
|
93
|
+
} else {
|
|
94
|
+
log("smartapp", `Running without root privileges - listening only on port ${configuredPort}`);
|
|
95
|
+
log("smartapp", `To enable port 80, run with sudo or use: sudo setcap 'cap_net_bind_service=+ep' $(which node)`);
|
|
96
|
+
this.port = configuredPort;
|
|
97
|
+
}
|
|
98
|
+
} else {
|
|
99
|
+
this.port = 80;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Initialize mDNS service
|
|
103
|
+
this.setupMDNS();
|
|
79
104
|
|
|
80
105
|
this.system = system;
|
|
81
106
|
this.power = power;
|
|
@@ -84,6 +109,7 @@ export class SmartApp extends WebApp {
|
|
|
84
109
|
// when all masters are loaded -> attach units to the switches
|
|
85
110
|
this.system.emitter.on('ready', this.initSwitchUnits.bind(this));
|
|
86
111
|
this.system.emitter.on('ready', this.initLinks.bind(this));
|
|
112
|
+
this.system.emitter.on('ready', this.initDeviceUnits.bind(this));
|
|
87
113
|
|
|
88
114
|
this.addFile("unitList", __dirname + "/views/unit-list.ejs", "application/json");
|
|
89
115
|
this.addFile("masterList", __dirname + "/views/master-list.ejs", "text/html");
|
|
@@ -97,19 +123,92 @@ export class SmartApp extends WebApp {
|
|
|
97
123
|
this.addFile("switchList", __dirname + "/views/switch-list.ejs", "text/html");
|
|
98
124
|
this.addFile("linkDetail", __dirname + "/views/link-details.ejs", "text/html");
|
|
99
125
|
this.addFile("linkList", __dirname + "/views/link-list.ejs", "text/html");
|
|
126
|
+
this.addFile("deviceDetail", __dirname + "/views/device-details.ejs", "text/html");
|
|
127
|
+
this.addFile("deviceList", __dirname + "/views/device-list.ejs", "text/html");
|
|
100
128
|
this.addFile("powerRule", __dirname + "/views/power-rule.ejs", "text/html");
|
|
101
129
|
this.addFile("powerBinding", __dirname + "/views/power-binding.ejs", "text/html");
|
|
102
130
|
this.addFile("materializeCSS", __dirname + "/views/assets/materialize.min.css", "text/css");
|
|
103
131
|
this.addFile("materializeJS", __dirname + "/views/assets/materialize.min.js", "text/javascript");
|
|
104
132
|
this.addFile("favicon", __dirname + "/views/assets/favicon.ico", "image/x-icon");
|
|
133
|
+
this.addFile("logoWhite", __dirname + "/views/assets/Duotecno_logo_white.svg", "image/svg+xml");
|
|
105
134
|
|
|
106
135
|
this.addFile("proxy", __dirname + "/views/proxy.ejs", "text/html");
|
|
107
136
|
}
|
|
108
137
|
|
|
138
|
+
// Override to use config.system.debug for template caching control
|
|
139
|
+
protected isDebugMode(): boolean {
|
|
140
|
+
return this.system?.config?.debug === true;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
setupMDNS() {
|
|
144
|
+
// Read settings to get configured mDNS name
|
|
145
|
+
const settings = this.read("settings") as SettingsConfig;
|
|
146
|
+
const mdnsName = settings?.mdnsName || "duotecno-gateway";
|
|
147
|
+
const version = require('../package.json').version;
|
|
148
|
+
|
|
149
|
+
// Initialize mDNS through parent class
|
|
150
|
+
this.initMDNS(mdnsName, version);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
serve(onConnected?: () => void) {
|
|
154
|
+
// Initialize WebSocket servers array
|
|
155
|
+
this.wsServers = [];
|
|
156
|
+
|
|
157
|
+
// Call parent serve method
|
|
158
|
+
super.serve(() => {
|
|
159
|
+
// Set up WebSocket servers on the HTTP servers
|
|
160
|
+
this.setupWebSocketServers();
|
|
161
|
+
|
|
162
|
+
// Register mDNS service after server is listening
|
|
163
|
+
this.registerMDNS();
|
|
164
|
+
|
|
165
|
+
// Call the original callback if provided
|
|
166
|
+
if (onConnected) {
|
|
167
|
+
onConnected();
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
setupWebSocketServers() {
|
|
173
|
+
// Get all HTTP servers
|
|
174
|
+
this.servers.forEach((httpServer, index) => {
|
|
175
|
+
const wss = new WebSocket.Server({ server: httpServer, path: '/devices' });
|
|
176
|
+
this.wsServers.push(wss);
|
|
177
|
+
|
|
178
|
+
wss.on('connection', (ws: WebSocket) => {
|
|
179
|
+
log("smartapp", `WebSocket client connected on server ${index}`);
|
|
180
|
+
|
|
181
|
+
// Send initial device list
|
|
182
|
+
this.sendDeviceList(ws);
|
|
183
|
+
|
|
184
|
+
ws.on('message', (message: string) => {
|
|
185
|
+
try {
|
|
186
|
+
const data = JSON.parse(message.toString());
|
|
187
|
+
this.handleDeviceMessage(ws, data);
|
|
188
|
+
} catch (e) {
|
|
189
|
+
err("smartapp", `Invalid WebSocket message: ${e.message}`);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
ws.on('close', () => {
|
|
194
|
+
log("smartapp", `WebSocket client disconnected from server ${index}`);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
ws.on('error', (error) => {
|
|
198
|
+
err("smartapp", `WebSocket error on server ${index}: ${error.message}`);
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
log("smartapp", `WebSocket server ${index} set up on path /devices`);
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
|
|
109
207
|
writeConfig() {
|
|
110
208
|
// copy switches into config, eliminate the runtime stuff (like unit)
|
|
111
209
|
this.config.switches = this.switches.map(s => Sanitizers.makeSwitchConfig(s));
|
|
112
210
|
this.config.links = this.links.map(s => Sanitizers.makeLinkConfig(s));
|
|
211
|
+
this.config.devices = this.devices.map(d => Sanitizers.makeDeviceConfig(d));
|
|
113
212
|
|
|
114
213
|
super.writeConfig();
|
|
115
214
|
}
|
|
@@ -121,6 +220,7 @@ export class SmartApp extends WebApp {
|
|
|
121
220
|
// copy switches from config
|
|
122
221
|
this.switches = this.config.switches.map(s => Sanitizers.switchConfig(s));
|
|
123
222
|
this.links = this.config.links.map(s => Sanitizers.linkConfig(s));
|
|
223
|
+
this.devices = (this.config.devices || []).map(d => Sanitizers.deviceConfig(d));
|
|
124
224
|
}
|
|
125
225
|
|
|
126
226
|
|
|
@@ -170,7 +270,7 @@ export class SmartApp extends WebApp {
|
|
|
170
270
|
//////////////////////////////
|
|
171
271
|
|
|
172
272
|
needsLogin(context: Context): boolean {
|
|
173
|
-
const noNeed = ((context.request === "units") || (context.request === "switches")
|
|
273
|
+
const noNeed = ((context.request === "units") || (context.request === "switches")) &&
|
|
174
274
|
((context.action == "press") || (context.action == "get") || (context.action == "set"));
|
|
175
275
|
return super.needsLogin(context) && (context.request != "files") && (!noNeed);
|
|
176
276
|
}
|
|
@@ -210,6 +310,9 @@ export class SmartApp extends WebApp {
|
|
|
210
310
|
} else if (context.request === "links") {
|
|
211
311
|
return this.doLinks(context);
|
|
212
312
|
|
|
313
|
+
} else if (context.request === "devices") {
|
|
314
|
+
return this.doDevices(context);
|
|
315
|
+
|
|
213
316
|
} else if (context.request === "switches") {
|
|
214
317
|
return this.doSwitches(context);
|
|
215
318
|
|
|
@@ -290,35 +393,61 @@ export class SmartApp extends WebApp {
|
|
|
290
393
|
// Settings //
|
|
291
394
|
//////////////
|
|
292
395
|
async doSettings(context: Context): Promise<HttpResponse> {
|
|
293
|
-
let config = this.read("settings");
|
|
396
|
+
let config: SettingsConfig = this.read("settings") as SettingsConfig;
|
|
397
|
+
let message = "";
|
|
294
398
|
|
|
295
399
|
if (context.action === "save") {
|
|
296
400
|
config = this.scrapeSettings(context);
|
|
297
401
|
this.write("settings", config);
|
|
402
|
+
message = "Settings saved successfully. Restart the server for changes to take effect.";
|
|
403
|
+
|
|
404
|
+
// Re-register mDNS with new name if changed
|
|
405
|
+
if (this.mdnsService) {
|
|
406
|
+
// Note: Full restart recommended for mDNS changes to take effect properly
|
|
407
|
+
log("smartapp", "Settings saved. mDNS name updated to: " + config.mdnsName);
|
|
408
|
+
}
|
|
298
409
|
|
|
299
410
|
} else if (context.action === "restart") {
|
|
300
411
|
context.request = "restart";
|
|
301
412
|
return this.doRestart(false)
|
|
302
413
|
|
|
303
414
|
} else if (context.action === "install") {
|
|
415
|
+
// Network IP configuration - temporarily disabled
|
|
416
|
+
// TODO: Re-implement DHCP configuration
|
|
417
|
+
message = "Network IP configuration is temporarily disabled. Use 'Save Settings' for mDNS configuration.";
|
|
418
|
+
|
|
419
|
+
/* Commented out until DHCP implementation is fixed
|
|
304
420
|
config = this.scrapeSettings(context);
|
|
305
421
|
context.request = "install";
|
|
306
422
|
this.write("settings", config);
|
|
307
423
|
this.writeDHCP(context, <SettingsConfig>config);
|
|
424
|
+
*/
|
|
308
425
|
|
|
309
426
|
} else if (context.action === "reset") {
|
|
427
|
+
// Reset to DHCP - temporarily disabled
|
|
428
|
+
// TODO: Re-implement DHCP reset
|
|
429
|
+
message = "DHCP reset is temporarily disabled.";
|
|
430
|
+
|
|
431
|
+
/* Commented out until DHCP implementation is fixed
|
|
310
432
|
config = { ...kEmptySettings };
|
|
311
433
|
this.resetDHCP(context);
|
|
312
434
|
this.write("settings", config);
|
|
313
435
|
return this.doReboot(context, false);
|
|
436
|
+
*/
|
|
314
437
|
|
|
315
438
|
} else if (context.action === "reboot") {
|
|
439
|
+
// System reboot - temporarily disabled
|
|
440
|
+
// TODO: Re-enable after testing
|
|
441
|
+
message = "System reboot is temporarily disabled.";
|
|
442
|
+
|
|
443
|
+
/* Commented out until we want to enable system reboot
|
|
316
444
|
return this.doReboot(context, false);
|
|
445
|
+
*/
|
|
317
446
|
|
|
318
447
|
} else {
|
|
319
|
-
|
|
448
|
+
// Just display the settings form
|
|
320
449
|
}
|
|
321
|
-
return this.ejs("settings", context, { config });
|
|
450
|
+
return this.ejs("settings", context, { config, message });
|
|
322
451
|
}
|
|
323
452
|
|
|
324
453
|
scrapeSettings(context: Context): SettingsConfig {
|
|
@@ -329,7 +458,8 @@ export class SmartApp extends WebApp {
|
|
|
329
458
|
nameservers: context.getParam({name: "nameservers", type: "string"}),
|
|
330
459
|
ip2: context.getParam({name: "ip2", type: "string"}),
|
|
331
460
|
gateway2: context.getParam({name: "gateway2", type: "string"})
|
|
332
|
-
}
|
|
461
|
+
},
|
|
462
|
+
mdnsName: context.getParam({name: "mdnsName", type: "string", default: "duotecno-gateway"})
|
|
333
463
|
});
|
|
334
464
|
}
|
|
335
465
|
|
|
@@ -518,18 +648,22 @@ export class SmartApp extends WebApp {
|
|
|
518
648
|
const hbValue = dt2hbvalue(u, link.hbValue as number, link.min, link.max);
|
|
519
649
|
|
|
520
650
|
if (typeof hbValue != "undefined") {
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
651
|
+
try {
|
|
652
|
+
if (hbValue === -1) {
|
|
653
|
+
log("smartapp", "-> Virtual unit send 'stop' for accessory: " + link.accName + " of Duotecno unit: " + u.getDisplayName());
|
|
654
|
+
const val = await hbStop(this.config.apiHost +":"+ this.config.apiPort, link.accId, link.accService, this.token);
|
|
655
|
+
if (typeof val != "undefined") link.hbValue = val;
|
|
656
|
+
|
|
657
|
+
} else if (hbValue !== link.hbValue) {
|
|
658
|
+
log("smartapp", "-> Setting HB value for accessory: " + link.accName + " to " + hbValue + " of Duotecno unit: " + u.getDisplayName() + " with value " + u.value);
|
|
659
|
+
link.hbValue = hbValue;
|
|
660
|
+
await hbSetState(this.config.apiHost +":"+ this.config.apiPort, link.accId, link.accService, hbValue, this.token);
|
|
661
|
+
|
|
662
|
+
} else {
|
|
663
|
+
debug("smartapp", "-> No change for accessory: " + link.accName + " to " + u.value + " / " + u.status);
|
|
664
|
+
}
|
|
665
|
+
} catch(e) {
|
|
666
|
+
err("smartapp", "informLinks: failed to set HB value for accessory " + link.accName + " -> " + e.toString());
|
|
533
667
|
}
|
|
534
668
|
}
|
|
535
669
|
}
|
|
@@ -629,6 +763,281 @@ export class SmartApp extends WebApp {
|
|
|
629
763
|
}
|
|
630
764
|
|
|
631
765
|
|
|
766
|
+
/////////////
|
|
767
|
+
// Devices //
|
|
768
|
+
/////////////
|
|
769
|
+
initDeviceUnits() {
|
|
770
|
+
log("smartapp", "Init " + this.devices.length + " Devices -> add units");
|
|
771
|
+
|
|
772
|
+
this.devices.forEach(device => {
|
|
773
|
+
device.unit ||= this.system.findUnit(this.system.findMaster(device.masterAddress, device.masterPort),
|
|
774
|
+
device.logicalNodeAddress, device.logicalAddress);
|
|
775
|
+
if (device.unit) {
|
|
776
|
+
device.relay = device.unit.status > 0;
|
|
777
|
+
} else {
|
|
778
|
+
log("smartapp", "** error ** missing device unit: " + hex(device.logicalNodeAddress)+"/"+hex(device.logicalAddress) + " **");
|
|
779
|
+
}
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
async doDevices(context: Context): Promise<HttpResponse> {
|
|
784
|
+
let inx: number = parseInt(context.id);
|
|
785
|
+
const units = this.system.allUsedUnits();
|
|
786
|
+
|
|
787
|
+
let message: string;
|
|
788
|
+
try {
|
|
789
|
+
if (context.action === "add") {
|
|
790
|
+
this.devices.push({...kEmptyDevice, measured: false});
|
|
791
|
+
inx = this.devices.length-1;
|
|
792
|
+
return this.ejs("deviceDetail", context, { device: this.devices[inx], id: inx,
|
|
793
|
+
masters: this.system.masters, units });
|
|
794
|
+
|
|
795
|
+
} else if (context.action === "edit") {
|
|
796
|
+
return this.ejs("deviceDetail", context, { device: this.devices[inx], id: inx,
|
|
797
|
+
masters: this.system.masters, units });
|
|
798
|
+
|
|
799
|
+
} else if (context.action === "delete") {
|
|
800
|
+
this.deleteDevice(inx);
|
|
801
|
+
|
|
802
|
+
} else if (context.action === "change") {
|
|
803
|
+
this.updateDevice(inx, this.scrapeDevice(context));
|
|
804
|
+
|
|
805
|
+
} else {
|
|
806
|
+
// possible new IP Nodes, hence Units could be online
|
|
807
|
+
this.initDeviceUnits();
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
} catch(e) {
|
|
811
|
+
message = e.toString();
|
|
812
|
+
}
|
|
813
|
+
return this.ejs("deviceList", context, { masters: this.system.masters, devices: this.devices, message });
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
scrapeDevice(context: Context): Device {
|
|
817
|
+
const { masterAddress, masterPort, logicalAddress, logicalNodeAddress } = this.scrapeUnit(context, '');
|
|
818
|
+
const name = context.getParam({name: "name", type: "string", default: "Device" });
|
|
819
|
+
const powerSource = context.getParam({name: "powerSource", type: "string", default: "none" });
|
|
820
|
+
const powerChannel = context.getParam({name: "powerChannel", type: "string", default: "" });
|
|
821
|
+
const estimatedPower = context.getParam({name: "estimatedPower", type: "integer", default: 0 });
|
|
822
|
+
|
|
823
|
+
// Calculate ID from node/unit
|
|
824
|
+
const id = logicalNodeAddress * 256 + logicalAddress;
|
|
825
|
+
|
|
826
|
+
// Measured is true if powerSource is not "none"
|
|
827
|
+
const measured = powerSource !== "none";
|
|
828
|
+
|
|
829
|
+
return {
|
|
830
|
+
id,
|
|
831
|
+
name,
|
|
832
|
+
masterAddress,
|
|
833
|
+
masterPort,
|
|
834
|
+
logicalAddress,
|
|
835
|
+
logicalNodeAddress,
|
|
836
|
+
measured,
|
|
837
|
+
powerSource: powerSource as any,
|
|
838
|
+
powerChannel,
|
|
839
|
+
estimatedPower,
|
|
840
|
+
power: 0,
|
|
841
|
+
energy: 0,
|
|
842
|
+
voltage: 230.0,
|
|
843
|
+
current: 0,
|
|
844
|
+
relay: false
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
updateDevice(inx: number, device: Device) {
|
|
849
|
+
if ((inx >= 0) && (inx < this.devices.length)) {
|
|
850
|
+
this.devices[inx] = device;
|
|
851
|
+
this.initDeviceUnits();
|
|
852
|
+
this.writeConfig();
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
deleteDevice(inx: number) {
|
|
857
|
+
if ((inx >= 0) && (inx < this.devices.length)) {
|
|
858
|
+
this.devices.splice(inx, 1);
|
|
859
|
+
this.initDeviceUnits();
|
|
860
|
+
this.writeConfig();
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
//////////////////////
|
|
865
|
+
// Device - sockets //
|
|
866
|
+
//////////////////////
|
|
867
|
+
sendDeviceList(ws: WebSocket) {
|
|
868
|
+
const devices = this.getDeviceList();
|
|
869
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
870
|
+
ws.send(JSON.stringify(devices));
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
getDeviceList(): Array<any> {
|
|
875
|
+
return this.devices.map(device => {
|
|
876
|
+
// Get current relay state from unit if available
|
|
877
|
+
let relayState = device.relay || false;
|
|
878
|
+
if (device.unit) {
|
|
879
|
+
relayState = device.unit.status > 0;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// Get power measurements based on power source
|
|
883
|
+
let power = 0;
|
|
884
|
+
let energy = 0;
|
|
885
|
+
let voltage = 230.0;
|
|
886
|
+
let current = 0;
|
|
887
|
+
let measured = false;
|
|
888
|
+
|
|
889
|
+
if (device.powerSource === PowerSource.kSmappee && this.power.smappee) {
|
|
890
|
+
// Get power from Smappee
|
|
891
|
+
const smappeePower = this.getPowerFromSmappee(device.powerChannel);
|
|
892
|
+
if (smappeePower !== null) {
|
|
893
|
+
power = smappeePower;
|
|
894
|
+
measured = true;
|
|
895
|
+
current = power / voltage;
|
|
896
|
+
}
|
|
897
|
+
} else if (device.powerSource === PowerSource.kP1 && this.power.p1) {
|
|
898
|
+
// Get power from P1 meter
|
|
899
|
+
const p1Power = this.getPowerFromP1(device.powerChannel);
|
|
900
|
+
if (p1Power !== null) {
|
|
901
|
+
power = p1Power;
|
|
902
|
+
measured = true;
|
|
903
|
+
current = power / voltage;
|
|
904
|
+
}
|
|
905
|
+
} else if (device.powerSource === PowerSource.kShelly && this.power.shelly) {
|
|
906
|
+
// Get power from Shelly
|
|
907
|
+
const shellyPower = this.getPowerFromShelly(device.powerChannel);
|
|
908
|
+
if (shellyPower !== null) {
|
|
909
|
+
power = shellyPower;
|
|
910
|
+
measured = true;
|
|
911
|
+
current = power / voltage;
|
|
912
|
+
}
|
|
913
|
+
} else if (device.powerSource === PowerSource.kNone || !device.powerSource) {
|
|
914
|
+
// Use estimated power when relay is on
|
|
915
|
+
power = relayState ? (device.estimatedPower || 0) : 0;
|
|
916
|
+
measured = false;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
return {
|
|
920
|
+
id: device.id,
|
|
921
|
+
name: device.name,
|
|
922
|
+
power: power,
|
|
923
|
+
energy: energy,
|
|
924
|
+
voltage: voltage,
|
|
925
|
+
current: current,
|
|
926
|
+
relay: relayState,
|
|
927
|
+
measured: measured
|
|
928
|
+
};
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
getPowerFromSmappee(channel: string): number | null {
|
|
933
|
+
if (!this.power.smappee) return null;
|
|
934
|
+
try {
|
|
935
|
+
// Channel format could be "0", "0+1", "0+1+2", etc.
|
|
936
|
+
const channels = channel.split('+').map(c => parseInt(c.trim()));
|
|
937
|
+
let totalPower = 0;
|
|
938
|
+
for (const ch of channels) {
|
|
939
|
+
const binding = this.power.smappee.bindings.find(b => b.channel.includes(ch.toString()));
|
|
940
|
+
if (binding && binding.value !== undefined) {
|
|
941
|
+
totalPower += binding.value;
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
return totalPower;
|
|
945
|
+
} catch (e) {
|
|
946
|
+
err("smartapp", `Error getting Smappee power for channel ${channel}: ${e.message}`);
|
|
947
|
+
return null;
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
getPowerFromP1(channel: string): number | null {
|
|
952
|
+
if (!this.power.p1) return null;
|
|
953
|
+
try {
|
|
954
|
+
// P1 meter typically has single power value
|
|
955
|
+
const binding = this.power.p1.bindings.find(b => b.channel.includes(channel));
|
|
956
|
+
if (binding && binding.value !== undefined) {
|
|
957
|
+
return binding.value;
|
|
958
|
+
}
|
|
959
|
+
return null;
|
|
960
|
+
} catch (e) {
|
|
961
|
+
err("smartapp", `Error getting P1 power for channel ${channel}: ${e.message}`);
|
|
962
|
+
return null;
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
getPowerFromShelly(channel: string): number | null {
|
|
967
|
+
if (!this.power.shelly) return null;
|
|
968
|
+
try {
|
|
969
|
+
// Shelly channel format similar to Smappee
|
|
970
|
+
const channels = channel.split('+').map(c => parseInt(c.trim()));
|
|
971
|
+
let totalPower = 0;
|
|
972
|
+
for (const ch of channels) {
|
|
973
|
+
const binding = this.power.shelly.bindings.find(b => b.channel.includes(ch.toString()));
|
|
974
|
+
if (binding && binding.value !== undefined) {
|
|
975
|
+
totalPower += binding.value;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
return totalPower;
|
|
979
|
+
} catch (e) {
|
|
980
|
+
err("smartapp", `Error getting Shelly power for channel ${channel}: ${e.message}`);
|
|
981
|
+
return null;
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
async handleDeviceMessage(ws: WebSocket, data: any) {
|
|
986
|
+
if (!Array.isArray(data)) {
|
|
987
|
+
err("smartapp", "WebSocket message must be an array");
|
|
988
|
+
return;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
// Empty array = keep-alive / poll
|
|
992
|
+
if (data.length === 0) {
|
|
993
|
+
this.sendDeviceList(ws);
|
|
994
|
+
return;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
// Process relay control commands
|
|
998
|
+
for (const cmd of data) {
|
|
999
|
+
if (cmd.id && cmd.relay !== undefined) {
|
|
1000
|
+
await this.setDeviceRelay(cmd.id, cmd.relay);
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
// Send updated device list
|
|
1005
|
+
this.sendDeviceList(ws);
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
async setDeviceRelay(id: number, state: boolean) {
|
|
1009
|
+
// Convert ID to node/unit (id = node * 256 + unit)
|
|
1010
|
+
const node = Math.floor(id / 256);
|
|
1011
|
+
const unit = id % 256;
|
|
1012
|
+
|
|
1013
|
+
log("smartapp", `Setting device ${id} (node=${node}, unit=${unit}) to ${state}`);
|
|
1014
|
+
|
|
1015
|
+
// Get the first master
|
|
1016
|
+
const master = this.system.masters[0];
|
|
1017
|
+
if (!master) {
|
|
1018
|
+
err("smartapp", "No master available");
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
// Find the unit
|
|
1023
|
+
const nodeObj = master.nodes.find(n => n.logicalAddress === node);
|
|
1024
|
+
if (!nodeObj) {
|
|
1025
|
+
err("smartapp", `Node ${node} not found`);
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
const unitObj = nodeObj.units.find(u => u.logicalAddress === unit);
|
|
1030
|
+
if (!unitObj) {
|
|
1031
|
+
err("smartapp", `Unit ${unit} in node ${node} not found`);
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
// Set the unit state (convert boolean to 0/100 for dimmer/switch)
|
|
1036
|
+
await master.setUnitStatus(unitObj, state ? 100 : 0);
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
|
|
1040
|
+
|
|
632
1041
|
// not used / tested for the moment
|
|
633
1042
|
async setLink(inx: number, newstate?: boolean, newvalue?: number)
|
|
634
1043
|
async setLink(inx: Link, newstate?: boolean, newvalue?: number)
|
|
@@ -1268,6 +1677,9 @@ export class SmartApp extends WebApp {
|
|
|
1268
1677
|
} else if (file === "favicon.ico") {
|
|
1269
1678
|
return this.file("favicon");
|
|
1270
1679
|
|
|
1680
|
+
} else if (file === "Duotecno_logo_white.svg") {
|
|
1681
|
+
return this.file("logoWhite");
|
|
1682
|
+
|
|
1271
1683
|
} else
|
|
1272
1684
|
return this.notFound();
|
|
1273
1685
|
}
|
package/server/socapp.js
CHANGED
|
@@ -66,7 +66,8 @@ class SocApp extends webapp_1.WebApp {
|
|
|
66
66
|
serve() {
|
|
67
67
|
(0, logger_1.log)("socapp", "SocApp - Start http server on port " + this.port);
|
|
68
68
|
super.serve(() => {
|
|
69
|
-
|
|
69
|
+
// Create WebSocket server on the first HTTP server
|
|
70
|
+
const wsServer = new ws_1.Server({ server: this.servers[0] });
|
|
70
71
|
wsServer.on('connection', this.handleClient.bind(this));
|
|
71
72
|
});
|
|
72
73
|
}
|
package/server/socapp.ts
CHANGED
|
@@ -63,7 +63,8 @@ export class SocApp extends WebApp {
|
|
|
63
63
|
serve() {
|
|
64
64
|
log("socapp", "SocApp - Start http server on port " + this.port);
|
|
65
65
|
super.serve(() => {
|
|
66
|
-
|
|
66
|
+
// Create WebSocket server on the first HTTP server
|
|
67
|
+
const wsServer = new WSServer({server: this.servers[0]});
|
|
67
68
|
wsServer.on('connection', this.handleClient.bind(this));
|
|
68
69
|
});
|
|
69
70
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<svg width="1550" height="1599" viewBox="0 0 1550 1599" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path d="M54.6003 5.06515C49.9542 9.71127 46.4696 243.179 46.4696 523.107C46.4696 1023.73 46.4696 1031.86 23.239 1031.86C-6.96081 1031.86 -8.12233 1078.32 22.0774 1086.45C41.8235 1091.1 44.1465 1107.36 48.7926 1279.26C54.6003 1515.05 73.1847 1558.03 177.722 1582.42C206.761 1589.39 209.084 1587.07 209.084 1548.74C209.084 1513.89 205.599 1508.08 181.207 1508.08C163.784 1508.08 142.877 1495.31 128.938 1477.89C106.869 1450.01 104.546 1431.42 104.546 1268.81V1089.93H156.815C204.438 1089.93 209.084 1087.61 209.084 1060.9C209.084 1034.18 204.438 1031.86 156.815 1031.86H104.546L102.223 518.461C98.7384 65.4647 96.4154 3.90362 81.3155 0.419028C70.8617 -0.742501 59.2464 0.419028 54.6003 5.06515Z" fill="black"/>
|
|
3
|
+
<path d="M778.233 216.463V340.747L721.318 361.655C649.303 387.208 586.581 456.9 563.35 537.046C541.281 609.061 541.281 641.583 562.189 711.275C593.55 816.974 679.503 890.151 775.91 895.958L818.887 898.281L822.371 494.069C824.694 96.8259 824.694 91.0182 801.464 91.0182C779.395 91.0182 778.233 97.9874 778.233 216.463ZM778.233 626.483V835.559L752.68 827.428C670.211 803.036 627.234 756.575 604.004 669.46C590.065 618.353 604.004 541.692 636.527 498.715C664.403 460.385 729.449 416.247 756.164 416.247C777.072 416.247 778.233 429.023 778.233 626.483Z" fill="black"/>
|
|
4
|
+
<path d="M1324.15 358.171C1246.33 441.801 1221.94 659.007 1277.69 785.614C1346.22 942.421 1461.21 938.936 1522.77 777.483C1554.14 695.015 1557.62 553.308 1529.74 462.709C1491.41 332.617 1393.84 283.833 1324.15 358.171ZM1450.76 423.217C1487.93 470.84 1501.87 532.401 1496.06 632.292C1489.09 776.322 1439.14 854.144 1374.1 823.945C1284.66 783.291 1269.56 509.17 1350.87 418.571C1382.23 384.886 1421.72 386.048 1450.76 423.217Z" fill="black"/>
|
|
5
|
+
<path d="M917.617 619.516C917.617 884.344 918.778 892.475 940.848 892.475C958.27 892.475 964.078 884.344 964.078 861.114V829.753L996.601 859.952C1063.97 924.998 1138.31 908.737 1160.38 826.268C1167.35 801.876 1173.15 684.561 1173.15 563.762C1173.15 354.687 1171.99 346.556 1149.92 346.556C1127.85 346.556 1126.69 354.687 1126.69 560.278C1126.69 678.754 1122.05 787.938 1115.08 804.199C1101.14 842.529 1062.81 843.691 1009.38 807.684L969.886 780.968L966.401 563.762C964.078 355.848 962.917 346.556 940.848 346.556C918.778 346.556 917.617 355.848 917.617 619.516Z" fill="black"/>
|
|
6
|
+
<path d="M343.821 1048.12C277.614 1117.81 250.899 1265.33 283.422 1391.93C313.621 1509.25 372.859 1570.81 464.62 1580.1C502.951 1583.58 505.274 1581.26 508.758 1545.25C512.243 1510.41 509.92 1508.09 476.236 1508.09C396.09 1508.09 336.852 1438.39 320.591 1324.56L313.621 1275.78H429.774H545.927V1237.45C545.927 1186.34 515.728 1089.93 490.174 1059.73C439.067 999.335 392.605 994.689 343.821 1048.12ZM448.359 1098.07C468.105 1117.81 499.466 1184.02 499.466 1206.09C499.466 1213.06 464.62 1217.7 412.351 1217.7C318.267 1217.7 313.621 1214.22 338.013 1151.5C364.729 1081.8 408.867 1059.73 448.359 1098.07Z" fill="black"/>
|
|
7
|
+
<path d="M1370.61 1016.76C1282.34 1049.28 1225.42 1230.48 1253.3 1387.29C1270.72 1486.02 1305.57 1552.22 1357.84 1582.42C1397.33 1604.49 1397.33 1604.49 1439.14 1582.42C1582.01 1504.6 1588.98 1116.65 1447.27 1030.7C1410.11 1007.47 1399.65 1006.3 1370.61 1016.76ZM1442.63 1107.36C1483.28 1148.01 1501.87 1223.51 1496.06 1330.37C1491.41 1433.75 1465.86 1497.63 1421.72 1518.54C1353.19 1548.74 1298.6 1454.65 1298.6 1307.14C1298.6 1204.93 1316.02 1144.53 1357.84 1102.71C1389.2 1071.35 1407.78 1072.51 1442.63 1107.36Z" fill="black"/>
|
|
8
|
+
<path d="M752.68 1031.86C651.627 1062.06 587.743 1167.76 587.743 1304.82C587.743 1454.65 667.888 1569.65 778.234 1580.1C816.564 1583.58 818.887 1581.26 822.372 1545.25C825.856 1511.57 823.533 1508.09 793.334 1508.09C708.542 1508.09 638.85 1417.49 638.85 1304.82C638.85 1195.63 691.119 1121.3 781.718 1102.71C813.08 1095.74 818.887 1089.93 818.887 1060.9C818.887 1022.57 803.787 1015.6 752.68 1031.86Z" fill="black"/>
|
|
9
|
+
<path d="M1045.39 1033.02C1029.12 1039.99 1004.73 1056.25 990.793 1069.03L964.078 1093.42V1063.22C964.078 1039.99 958.27 1031.86 940.848 1031.86C918.778 1031.86 917.617 1039.99 917.617 1304.82C917.617 1569.65 918.778 1577.78 940.848 1577.78C962.917 1577.78 964.078 1569.65 964.078 1368.7C964.078 1161.95 964.078 1158.46 991.955 1130.59C1034.93 1087.61 1079.07 1080.64 1105.78 1113.16C1124.37 1136.39 1126.69 1163.11 1126.69 1358.25C1126.69 1569.65 1127.85 1577.78 1149.92 1577.78C1171.99 1577.78 1173.15 1569.65 1173.15 1359.41C1173.15 1145.69 1163.86 1069.03 1134.82 1045.8C1110.43 1026.05 1075.59 1021.4 1045.39 1033.02Z" fill="black"/>
|
|
10
|
+
</svg>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<svg width="1550" height="1599" viewBox="0 0 1550 1599" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path d="M54.6003 5.06515C49.9542 9.71127 46.4696 243.179 46.4696 523.107C46.4696 1023.73 46.4696 1031.86 23.239 1031.86C-6.96081 1031.86 -8.12233 1078.32 22.0774 1086.45C41.8235 1091.1 44.1465 1107.36 48.7926 1279.26C54.6003 1515.05 73.1847 1558.03 177.722 1582.42C206.761 1589.39 209.084 1587.07 209.084 1548.74C209.084 1513.89 205.599 1508.08 181.207 1508.08C163.784 1508.08 142.877 1495.31 128.938 1477.89C106.869 1450.01 104.546 1431.42 104.546 1268.81V1089.93H156.815C204.438 1089.93 209.084 1087.61 209.084 1060.9C209.084 1034.18 204.438 1031.86 156.815 1031.86H104.546L102.223 518.461C98.7384 65.4647 96.4154 3.90362 81.3155 0.419028C70.8617 -0.742501 59.2464 0.419028 54.6003 5.06515Z" fill="white"/>
|
|
3
|
+
<path d="M778.233 216.463V340.747L721.318 361.655C649.304 387.208 586.581 456.9 563.35 537.046C541.281 609.061 541.281 641.583 562.189 711.275C593.55 816.974 679.503 890.151 775.91 895.958L818.887 898.281L822.371 494.069C824.695 96.8259 824.695 91.0182 801.464 91.0182C779.395 91.0182 778.233 97.9874 778.233 216.463ZM778.233 626.483V835.559L752.68 827.428C670.211 803.036 627.234 756.575 604.004 669.46C590.066 618.353 604.004 541.692 636.527 498.715C664.403 460.385 729.449 416.247 756.164 416.247C777.072 416.247 778.233 429.023 778.233 626.483Z" fill="white"/>
|
|
4
|
+
<path d="M1324.15 358.171C1246.33 441.801 1221.94 659.007 1277.69 785.614C1346.22 942.421 1461.21 938.936 1522.77 777.483C1554.14 695.015 1557.62 553.308 1529.74 462.709C1491.41 332.617 1393.84 283.833 1324.15 358.171ZM1450.76 423.217C1487.93 470.84 1501.87 532.401 1496.06 632.292C1489.09 776.322 1439.14 854.144 1374.1 823.945C1284.66 783.291 1269.56 509.17 1350.87 418.571C1382.23 384.886 1421.72 386.048 1450.76 423.217Z" fill="white"/>
|
|
5
|
+
<path d="M917.617 619.516C917.617 884.344 918.778 892.475 940.848 892.475C958.27 892.475 964.078 884.344 964.078 861.114V829.753L996.601 859.952C1063.97 924.998 1138.31 908.737 1160.38 826.268C1167.35 801.876 1173.15 684.561 1173.15 563.762C1173.15 354.687 1171.99 346.556 1149.92 346.556C1127.85 346.556 1126.69 354.687 1126.69 560.278C1126.69 678.754 1122.05 787.938 1115.08 804.199C1101.14 842.529 1062.81 843.691 1009.38 807.684L969.886 780.968L966.401 563.762C964.078 355.848 962.917 346.556 940.848 346.556C918.778 346.556 917.617 355.848 917.617 619.516Z" fill="white"/>
|
|
6
|
+
<path d="M343.821 1048.12C277.614 1117.81 250.899 1265.33 283.422 1391.93C313.621 1509.25 372.859 1570.81 464.62 1580.1C502.951 1583.58 505.274 1581.26 508.758 1545.25C512.243 1510.41 509.92 1508.09 476.236 1508.09C396.09 1508.09 336.852 1438.39 320.591 1324.56L313.621 1275.78H429.774H545.927V1237.45C545.927 1186.34 515.728 1089.93 490.174 1059.73C439.067 999.335 392.605 994.689 343.821 1048.12ZM448.359 1098.07C468.105 1117.81 499.466 1184.02 499.466 1206.09C499.466 1213.06 464.62 1217.7 412.351 1217.7C318.267 1217.7 313.621 1214.22 338.013 1151.5C364.729 1081.8 408.867 1059.73 448.359 1098.07Z" fill="white"/>
|
|
7
|
+
<path d="M1370.61 1016.76C1282.34 1049.28 1225.42 1230.48 1253.3 1387.29C1270.72 1486.02 1305.57 1552.22 1357.84 1582.42C1397.33 1604.49 1397.33 1604.49 1439.14 1582.42C1582.01 1504.6 1588.98 1116.65 1447.27 1030.7C1410.11 1007.47 1399.65 1006.3 1370.61 1016.76ZM1442.63 1107.36C1483.28 1148.01 1501.87 1223.51 1496.06 1330.37C1491.41 1433.75 1465.86 1497.63 1421.72 1518.54C1353.19 1548.74 1298.6 1454.65 1298.6 1307.14C1298.6 1204.93 1316.02 1144.53 1357.84 1102.71C1389.2 1071.35 1407.78 1072.51 1442.63 1107.36Z" fill="white"/>
|
|
8
|
+
<path d="M752.68 1031.86C651.627 1062.06 587.743 1167.76 587.743 1304.82C587.743 1454.65 667.888 1569.65 778.234 1580.1C816.564 1583.58 818.887 1581.26 822.372 1545.25C825.856 1511.57 823.533 1508.09 793.333 1508.09C708.542 1508.09 638.85 1417.49 638.85 1304.82C638.85 1195.63 691.119 1121.3 781.718 1102.71C813.079 1095.74 818.887 1089.93 818.887 1060.9C818.887 1022.57 803.787 1015.6 752.68 1031.86Z" fill="white"/>
|
|
9
|
+
<path d="M1045.39 1033.02C1029.12 1039.99 1004.73 1056.25 990.793 1069.03L964.078 1093.42V1063.22C964.078 1039.99 958.27 1031.86 940.848 1031.86C918.778 1031.86 917.617 1039.99 917.617 1304.82C917.617 1569.65 918.778 1577.78 940.848 1577.78C962.917 1577.78 964.078 1569.65 964.078 1368.7C964.078 1161.95 964.078 1158.46 991.955 1130.59C1034.93 1087.61 1079.07 1080.64 1105.78 1113.16C1124.37 1136.39 1126.69 1163.11 1126.69 1358.25C1126.69 1569.65 1127.85 1577.78 1149.92 1577.78C1171.99 1577.78 1173.15 1569.65 1173.15 1359.41C1173.15 1145.69 1163.86 1069.03 1134.82 1045.8C1110.43 1026.05 1075.59 1021.4 1045.39 1033.02Z" fill="white"/>
|
|
10
|
+
</svg>
|