@atelieruldetehnologii/node-red-custome-solutions 0.62.22

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.
Files changed (107) hide show
  1. package/NOTICE +56 -0
  2. package/PROPRIETARY-LICENSE.txt +157 -0
  3. package/README.md +105 -0
  4. package/TERMS-AND-CONDITIONS.md +311 -0
  5. package/advantech-read/advantech-read.html +775 -0
  6. package/advantech-read/advantech-read.js +449 -0
  7. package/advantech-write/advantech-write.html +413 -0
  8. package/advantech-write/advantech-write.js +338 -0
  9. package/bess-control/bess-control.html +569 -0
  10. package/bess-control/bess-control.js +541 -0
  11. package/bess-control/lib/allocation.js +149 -0
  12. package/bess-control/lib/batteries.js +86 -0
  13. package/bess-control/lib/helpers.js +157 -0
  14. package/bess-control/lib/messages.js +131 -0
  15. package/bess-control/lib/runtime.js +156 -0
  16. package/carlo-gavazzi/carlo-gavazzi.html +4327 -0
  17. package/carlo-gavazzi/carlo-gavazzi.js +3081 -0
  18. package/control-export/control-export.html +3644 -0
  19. package/control-export/control-export.js +2192 -0
  20. package/control-reactive/control-reactive.html +3136 -0
  21. package/control-reactive/control-reactive.js +2124 -0
  22. package/fronius-emulator/README.md +258 -0
  23. package/fronius-emulator/fronius-emulator.html +170 -0
  24. package/fronius-emulator/fronius-emulator.js +520 -0
  25. package/fronius-emulator/lib/binary-codec.js +65 -0
  26. package/fronius-emulator/lib/fronius-register-map.js +185 -0
  27. package/fronius-emulator/lib/modbus-parser.js +61 -0
  28. package/fronius-emulator/lib/modbus-tcp-server.js +224 -0
  29. package/fronius-emulator/lib/validation.js +206 -0
  30. package/fronius-emulator/lib/virtual-meter.js +132 -0
  31. package/fronius-read/fronius-read.html +3944 -0
  32. package/fronius-read/fronius-read.js +1466 -0
  33. package/fronius-read/lib/inverter-profiles.js +711 -0
  34. package/fronius-read/lib/sunspec-core.js +303 -0
  35. package/fronius-read/lib/sunspec-model-parsers.js +845 -0
  36. package/fronius-write/fronius-write.html +1789 -0
  37. package/fronius-write/fronius-write.js +2531 -0
  38. package/influxdb/influxdb.html +1077 -0
  39. package/influxdb/influxdb.js +1487 -0
  40. package/janitza/janitza.html +2562 -0
  41. package/janitza/janitza.js +1500 -0
  42. package/janitza/lib/janitza-profiles.js +371 -0
  43. package/lib/a3-test-server.js +199 -0
  44. package/lib/adam-6000-profiles.js +173 -0
  45. package/lib/licensing/constants.js +103 -0
  46. package/lib/licensing/device-id.js +251 -0
  47. package/lib/licensing/errors.js +21 -0
  48. package/lib/licensing/license-client.js +377 -0
  49. package/lib/licensing/license-state.js +50 -0
  50. package/lib/licensing/node-guard.js +150 -0
  51. package/lib/licensing/package-integrity.js +167 -0
  52. package/lib/licensing/package-integrity.json +513 -0
  53. package/lib/licensing/public-key.pem +3 -0
  54. package/lib/licensing/public-key.pem.example +2 -0
  55. package/lib/licensing/public-keys.js +18 -0
  56. package/lib/licensing/storage.js +63 -0
  57. package/lib/licensing/token-verifier.js +84 -0
  58. package/lib/numbers.js +26 -0
  59. package/license/atelier-license.html +88 -0
  60. package/license/atelier-license.js +170 -0
  61. package/modbus-tcp-discover/modbus-tcp-discover.html +190 -0
  62. package/modbus-tcp-discover/modbus-tcp-discover.js +706 -0
  63. package/multi-battery/multi-battery.html +649 -0
  64. package/multi-battery/multi-battery.js +668 -0
  65. package/multi-grid/multi-grid.html +745 -0
  66. package/multi-grid/multi-grid.js +1392 -0
  67. package/multi-inverter/lib/engine-core.js +237 -0
  68. package/multi-inverter/multi-inverter.html +2674 -0
  69. package/multi-inverter/multi-inverter.js +1393 -0
  70. package/package.json +109 -0
  71. package/phoenix-contact/lib/eem-ma370-advanced.js +175 -0
  72. package/phoenix-contact/lib/eem-ma370.js +138 -0
  73. package/phoenix-contact/lib/integrations.js +149 -0
  74. package/phoenix-contact/lib/phoenix-contact-profile.js +35030 -0
  75. package/phoenix-contact/phoenix-contact.html +840 -0
  76. package/phoenix-contact/phoenix-contact.js +679 -0
  77. package/pv-generator/pv-generator.html +284 -0
  78. package/pv-generator/pv-generator.js +387 -0
  79. package/pylontech-read/pylontech-read.html +763 -0
  80. package/pylontech-read/pylontech-read.js +518 -0
  81. package/pylontech-write/pylontech-write.html +405 -0
  82. package/pylontech-write/pylontech-write.js +546 -0
  83. package/scada-aggregator/lib/aggregate.js +72 -0
  84. package/scada-aggregator/lib/groups.js +39 -0
  85. package/scada-aggregator/scada-aggregator.html +108 -0
  86. package/scada-aggregator/scada-aggregator.js +117 -0
  87. package/scada-server/lib/aggregator-map.js +58 -0
  88. package/scada-server/lib/encoding.js +235 -0
  89. package/scada-server/lib/modbus-server.js +492 -0
  90. package/scada-server/lib/object-path.js +18 -0
  91. package/scada-server/lib/register-map.js +288 -0
  92. package/scada-server/scada-server.html +1419 -0
  93. package/scada-server/scada-server.js +704 -0
  94. package/scripts/check-syntax.js +66 -0
  95. package/scripts/package-integrity.js +46 -0
  96. package/scripts/package-release.js +116 -0
  97. package/scripts/run-tests.js +2178 -0
  98. package/scripts/tgz-integrity.js +74 -0
  99. package/sma-read/lib/sunspec-core.js +83 -0
  100. package/sma-read/lib/sunspec-model-parsers.js +853 -0
  101. package/sma-read/sma-read.html +659 -0
  102. package/sma-read/sma-read.js +458 -0
  103. package/sma-write/sma-write.html +105 -0
  104. package/sma-write/sma-write.js +270 -0
  105. package/victron-read/lib/mqtt-state.js +176 -0
  106. package/victron-read/victron-read.html +1074 -0
  107. package/victron-read/victron-read.js +1222 -0
@@ -0,0 +1,173 @@
1
+ "use strict";
2
+
3
+ const MODULE_NAME_ADDRESS = 40211;
4
+
5
+ const PROFILES = {
6
+ "6015": { model: "ADAM-6015", diCount: 0, doCount: 0, aiCount: 7, aoCount: 0 },
7
+ "6017": { model: "ADAM-6017", diCount: 0, doCount: 2, aiCount: 8, aoCount: 0 },
8
+ "6018": { model: "ADAM-6018", diCount: 0, doCount: 8, aiCount: 8, aoCount: 0 },
9
+ "6018+": { model: "ADAM-6018+", diCount: 0, doCount: 0, aiCount: 8, aoCount: 0 },
10
+ "6022": { model: "ADAM-6022", diCount: 2, doCount: 2, aiCount: 6, aoCount: 2 },
11
+ "6024": { model: "ADAM-6024", diCount: 2, doCount: 2, aiCount: 6, aoCount: 2 },
12
+ "6050": { model: "ADAM-6050", diCount: 12, doCount: 6, aiCount: 0, aoCount: 0 },
13
+ "6051": { model: "ADAM-6051", diCount: 12, doCount: 2, aiCount: 0, aoCount: 0 },
14
+ "6052": { model: "ADAM-6052", diCount: 8, doCount: 8, aiCount: 0, aoCount: 0 },
15
+ "6060": { model: "ADAM-6060", diCount: 6, doCount: 6, aiCount: 0, aoCount: 0 },
16
+ "6066": { model: "ADAM-6066", diCount: 6, doCount: 6, aiCount: 0, aoCount: 0 }
17
+ };
18
+
19
+ function cloneProfile(profile) {
20
+ return Object.assign({}, profile || {});
21
+ }
22
+
23
+ function normalizeModel(value) {
24
+ const text = String(value || "").toUpperCase();
25
+ const match = text.match(/60\d{2}\+?/);
26
+ return match ? match[0] : "";
27
+ }
28
+
29
+ function profileForModel(value) {
30
+ const key = normalizeModel(value);
31
+ if (key && PROFILES[key]) return cloneProfile(PROFILES[key]);
32
+ return null;
33
+ }
34
+
35
+ function decodeModuleWords(words) {
36
+ const values = Array.isArray(words) ? words : [];
37
+ const candidates = [];
38
+
39
+ values.forEach(function (word) {
40
+ const n = Number(word);
41
+ if (!Number.isFinite(n)) return;
42
+ candidates.push(String(n));
43
+ candidates.push(n.toString(16).padStart(4, "0"));
44
+
45
+ const hi = (n >> 8) & 0xff;
46
+ const lo = n & 0xff;
47
+ if (hi >= 32 && hi <= 126 && lo >= 32 && lo <= 126) {
48
+ candidates.push(String.fromCharCode(hi) + String.fromCharCode(lo));
49
+ }
50
+ });
51
+
52
+ if (values.length >= 2) {
53
+ candidates.push(values.map(function (word) {
54
+ return Number(word).toString(16).padStart(4, "0");
55
+ }).join(""));
56
+ candidates.push(values.map(function (word) {
57
+ const n = Number(word) || 0;
58
+ return String.fromCharCode((n >> 8) & 0xff) + String.fromCharCode(n & 0xff);
59
+ }).join(""));
60
+ }
61
+
62
+ for (let i = 0; i < candidates.length; i += 1) {
63
+ const model = normalizeModel(candidates[i]);
64
+ if (model) return model;
65
+ }
66
+
67
+ return "";
68
+ }
69
+
70
+ async function readModuleProfile(client, resolveAddress) {
71
+ const start = resolveAddress(MODULE_NAME_ADDRESS, "holding");
72
+ const response = await client.readHoldingRegisters(start, 2);
73
+ const words = response && Array.isArray(response.data) ? response.data : [];
74
+ const modelCode = decodeModuleWords(words);
75
+ const profile = profileForModel(modelCode);
76
+
77
+ if (!profile) {
78
+ return {
79
+ model: modelCode ? "ADAM-" + modelCode : "",
80
+ modelCode: modelCode,
81
+ rawModuleWords: words,
82
+ detectedBy: "module_register"
83
+ };
84
+ }
85
+
86
+ profile.modelCode = modelCode;
87
+ profile.rawModuleWords = words;
88
+ profile.detectedBy = "module_register";
89
+ return profile;
90
+ }
91
+
92
+ async function canReadCoils(client, start, count) {
93
+ if (count <= 0) return false;
94
+ try {
95
+ await client.readCoils(start, count);
96
+ return true;
97
+ } catch (e) {
98
+ return false;
99
+ }
100
+ }
101
+
102
+ async function canReadHolding(client, start, count) {
103
+ if (count <= 0) return false;
104
+ try {
105
+ await client.readHoldingRegisters(start, count);
106
+ return true;
107
+ } catch (e) {
108
+ return false;
109
+ }
110
+ }
111
+
112
+ async function probeProfile(client, resolveAddress) {
113
+ const profile = {
114
+ model: "ADAM-6000",
115
+ modelCode: "",
116
+ diCount: 0,
117
+ doCount: 0,
118
+ aiCount: 0,
119
+ aoCount: 0,
120
+ detectedBy: "probe"
121
+ };
122
+ const candidates = [
123
+ { key: "diCount", area: "coil", start: 1, counts: [14, 12, 8, 6, 2] },
124
+ { key: "doCount", area: "coil", start: 17, counts: [8, 6, 2] },
125
+ { key: "aiCount", area: "holding", start: 40001, counts: [8, 7, 6] },
126
+ { key: "aoCount", area: "holding", start: 40011, counts: [2] }
127
+ ];
128
+
129
+ for (let i = 0; i < candidates.length; i += 1) {
130
+ const candidate = candidates[i];
131
+ const start = resolveAddress(candidate.start, candidate.area);
132
+ for (let j = 0; j < candidate.counts.length; j += 1) {
133
+ const count = candidate.counts[j];
134
+ const ok = candidate.area === "coil"
135
+ ? await canReadCoils(client, start, count)
136
+ : await canReadHolding(client, start, count);
137
+ if (ok) {
138
+ profile[candidate.key] = count;
139
+ break;
140
+ }
141
+ }
142
+ }
143
+
144
+ return profile;
145
+ }
146
+
147
+ async function detectAdamProfile(client, resolveAddress, configuredModel) {
148
+ const configured = profileForModel(configuredModel);
149
+ if (configured) {
150
+ configured.detectedBy = "configured";
151
+ configured.modelCode = normalizeModel(configuredModel);
152
+ return configured;
153
+ }
154
+
155
+ try {
156
+ const detected = await readModuleProfile(client, resolveAddress);
157
+ if (detected && (detected.diCount || detected.doCount || detected.aiCount || detected.aoCount)) {
158
+ return detected;
159
+ }
160
+ } catch (e) {
161
+ // Some modules do not expose 40211 consistently; probing is the fallback.
162
+ }
163
+
164
+ return probeProfile(client, resolveAddress);
165
+ }
166
+
167
+ module.exports = {
168
+ MODULE_NAME_ADDRESS,
169
+ PROFILES,
170
+ detectAdamProfile,
171
+ profileForModel,
172
+ normalizeModel
173
+ };
@@ -0,0 +1,103 @@
1
+ "use strict";
2
+
3
+ const LICENSE_STATES = Object.freeze({
4
+ UNKNOWN: "UNKNOWN",
5
+ NOT_ACTIVATED: "NOT_ACTIVATED",
6
+ ACTIVATING: "ACTIVATING",
7
+ ACTIVE: "ACTIVE",
8
+ OFFLINE: "OFFLINE",
9
+ GRACE_PERIOD: "GRACE_PERIOD",
10
+ PENDING_APPROVAL: "PENDING_APPROVAL",
11
+ DEVICE_LIMIT_REACHED: "DEVICE_LIMIT_REACHED",
12
+ EXPIRED: "EXPIRED",
13
+ REVOKED: "REVOKED",
14
+ SUSPENDED: "SUSPENDED",
15
+ DEVICE_BLOCKED: "DEVICE_BLOCKED",
16
+ DEVICE_REJECTED: "DEVICE_REJECTED",
17
+ DEVICE_DEACTIVATED: "DEVICE_DEACTIVATED",
18
+ DEVICE_REMOVED: "DEVICE_REMOVED",
19
+ PACKAGE_TAMPERED: "PACKAGE_TAMPERED",
20
+ INVALID_TOKEN: "INVALID_TOKEN",
21
+ INVALID_DEVICE: "INVALID_DEVICE",
22
+ PRODUCT_NOT_LICENSED: "PRODUCT_NOT_LICENSED",
23
+ VERSION_NOT_LICENSED: "VERSION_NOT_LICENSED",
24
+ SERVER_ERROR: "SERVER_ERROR"
25
+ });
26
+
27
+ const LICENSE_TYPES = Object.freeze({
28
+ SUBSCRIPTION: "SUBSCRIPTION",
29
+ PERMANENT: "PERMANENT"
30
+ });
31
+
32
+ const LICENSED_STATES = Object.freeze([
33
+ LICENSE_STATES.ACTIVE,
34
+ LICENSE_STATES.OFFLINE,
35
+ LICENSE_STATES.GRACE_PERIOD
36
+ ]);
37
+
38
+ const SERVER_STATE_MAP = Object.freeze({
39
+ LICENSE_REVOKED: LICENSE_STATES.REVOKED,
40
+ LICENSE_EXPIRED: LICENSE_STATES.EXPIRED,
41
+ LICENSE_SUSPENDED: LICENSE_STATES.SUSPENDED,
42
+ COMPANY_SUSPENDED: LICENSE_STATES.SUSPENDED,
43
+ DEVICE_BLOCKED: LICENSE_STATES.DEVICE_BLOCKED,
44
+ DEVICE_REJECTED: LICENSE_STATES.DEVICE_REJECTED,
45
+ DEVICE_DEACTIVATED: LICENSE_STATES.DEVICE_DEACTIVATED,
46
+ DEVICE_REMOVED: LICENSE_STATES.DEVICE_REMOVED,
47
+ PRODUCT_NOT_ALLOWED: LICENSE_STATES.PRODUCT_NOT_LICENSED,
48
+ VERSION_NOT_ALLOWED: LICENSE_STATES.VERSION_NOT_LICENSED,
49
+ DEVICE_APPROVAL_REQUIRED: LICENSE_STATES.PENDING_APPROVAL,
50
+ COMPANY_DEVICE_LIMIT_REACHED: LICENSE_STATES.DEVICE_LIMIT_REACHED
51
+ });
52
+
53
+ const STATUS_PRESENTATION = Object.freeze({
54
+ UNKNOWN: { fill: "grey", shape: "ring", text: "license checking" },
55
+ NOT_ACTIVATED: { fill: "grey", shape: "ring", text: "not licensed" },
56
+ ACTIVATING: { fill: "blue", shape: "ring", text: "activating license" },
57
+ ACTIVE: { fill: "green", shape: "dot", text: "licensed" },
58
+ OFFLINE: { fill: "yellow", shape: "ring", text: "licensed offline" },
59
+ GRACE_PERIOD: { fill: "yellow", shape: "dot", text: "license grace period" },
60
+ PENDING_APPROVAL: { fill: "yellow", shape: "ring", text: "license pending" },
61
+ DEVICE_LIMIT_REACHED: { fill: "red", shape: "ring", text: "device limit reached" },
62
+ EXPIRED: { fill: "red", shape: "ring", text: "license expired" },
63
+ REVOKED: { fill: "red", shape: "dot", text: "license revoked" },
64
+ SUSPENDED: { fill: "red", shape: "dot", text: "license suspended" },
65
+ DEVICE_BLOCKED: { fill: "red", shape: "dot", text: "device blocked" },
66
+ DEVICE_REJECTED: { fill: "red", shape: "dot", text: "device rejected" },
67
+ DEVICE_DEACTIVATED: { fill: "red", shape: "dot", text: "device deactivated" },
68
+ DEVICE_REMOVED: { fill: "grey", shape: "ring", text: "device removed; deploy to activate again" },
69
+ PACKAGE_TAMPERED: { fill: "red", shape: "dot", text: "package tampered" },
70
+ INVALID_TOKEN: { fill: "red", shape: "ring", text: "invalid license token" },
71
+ INVALID_DEVICE: { fill: "red", shape: "ring", text: "invalid license device" },
72
+ PRODUCT_NOT_LICENSED: { fill: "red", shape: "ring", text: "product not licensed" },
73
+ VERSION_NOT_LICENSED: { fill: "red", shape: "ring", text: "version not licensed" },
74
+ SERVER_ERROR: { fill: "red", shape: "ring", text: "license server error" }
75
+ });
76
+
77
+ const PRODUCT_CODE = "node-red-custome-solutions";
78
+ const PRODUCT_NAME = "Atelierul de Tehnologii - NodeRed Custome Solutions";
79
+ const DEFAULT_SERVER_URL = "https://license.atelieruldetehnologii.ro";
80
+ const REQUEST_TIMEOUT_MS = 10000;
81
+ // Administrative changes (approval, blocking and company/license suspension)
82
+ // must not wait for the policy refresh interval, which is intentionally
83
+ // configured in minutes. This lightweight status check is authoritative.
84
+ const CONTROL_STATUS_INTERVAL_MS = 5 * 1000;
85
+ // Wake frequently enough to honor refresh policies configured in minutes.
86
+ // HTTP requests are still made only when refreshAfter/nextCheck is reached.
87
+ const SCHEDULER_INTERVAL_MS = 30 * 1000;
88
+ const RETRY_DELAYS_MS = Object.freeze([60 * 1000, 5 * 60 * 1000, 15 * 60 * 1000, 60 * 60 * 1000]);
89
+
90
+ module.exports = {
91
+ DEFAULT_SERVER_URL,
92
+ CONTROL_STATUS_INTERVAL_MS,
93
+ LICENSED_STATES,
94
+ LICENSE_STATES,
95
+ LICENSE_TYPES,
96
+ PRODUCT_CODE,
97
+ PRODUCT_NAME,
98
+ REQUEST_TIMEOUT_MS,
99
+ RETRY_DELAYS_MS,
100
+ SCHEDULER_INTERVAL_MS,
101
+ SERVER_STATE_MAP,
102
+ STATUS_PRESENTATION
103
+ };
@@ -0,0 +1,251 @@
1
+ "use strict";
2
+
3
+ const crypto = require("crypto");
4
+ const fs = require("fs");
5
+ const os = require("os");
6
+ const path = require("path");
7
+ const { ensurePrivateDirectory } = require("./storage");
8
+
9
+ function readText(filePath) {
10
+ try { return fs.readFileSync(filePath, "utf8").replace(/\0/g, "").trim(); } catch (error) { return ""; }
11
+ }
12
+
13
+ const VIRTUAL_INTERFACE = /^(?:lo|docker|br-|veth|virbr|vmnet|vboxnet|tun|tap|wg|tailscale|zt|podman|cni)/i;
14
+ const IDENTITY_CACHE_FILE = "device-identity.json";
15
+ const DEFAULT_MAC_RETRY_DELAYS_MS = [250, 750, 1500];
16
+
17
+ function normalizeMac(value) {
18
+ const normalized = String(value || "").trim().replace(/-/g, ":").toLowerCase();
19
+ if (!/^[0-9a-f]{2}(?::[0-9a-f]{2}){5}$/.test(normalized)) return null;
20
+ if (normalized === "00:00:00:00:00:00" || normalized === "ff:ff:ff:ff:ff:ff") return null;
21
+ return normalized;
22
+ }
23
+
24
+ function interfacePriority(name) {
25
+ if (/^(?:eth|en)/i.test(name)) return 0;
26
+ if (/^(?:wlan|wl|wifi)/i.test(name)) return 1;
27
+ return 2;
28
+ }
29
+
30
+ function macCandidates(networks) {
31
+ const candidates = [];
32
+ Object.entries(networks === undefined ? os.networkInterfaces() : networks).forEach(function ([name, addresses]) {
33
+ if (VIRTUAL_INTERFACE.test(name)) return;
34
+ (addresses || []).forEach(function (address) {
35
+ if (!address || address.internal) return;
36
+ const mac = normalizeMac(address.mac);
37
+ if (mac) candidates.push({ mac, name, priority: interfacePriority(name) });
38
+ });
39
+ });
40
+ return candidates;
41
+ }
42
+
43
+ function sysfsMacCandidates(sysfsRoot) {
44
+ const root = sysfsRoot === undefined ? "/sys/class/net" : sysfsRoot;
45
+ const candidates = [];
46
+ let names = [];
47
+ try { names = fs.readdirSync(root); } catch (error) { return candidates; }
48
+ names.forEach(function (name) {
49
+ if (VIRTUAL_INTERFACE.test(name)) return;
50
+ const mac = normalizeMac(readText(path.join(root, name, "address")));
51
+ if (mac) candidates.push({ mac, name, priority: interfacePriority(name) });
52
+ });
53
+ return candidates;
54
+ }
55
+
56
+ function physicalMacCandidates(networks, sysfsRoot) {
57
+ const unique = new Map();
58
+ macCandidates(networks).concat(sysfsMacCandidates(sysfsRoot)).forEach(function (candidate) {
59
+ const key = candidate.name + "\0" + candidate.mac;
60
+ if (!unique.has(key)) unique.set(key, candidate);
61
+ });
62
+ const candidates = Array.from(unique.values());
63
+ candidates.sort(function (left, right) {
64
+ return left.priority - right.priority || left.name.localeCompare(right.name) || left.mac.localeCompare(right.mac);
65
+ });
66
+ return candidates;
67
+ }
68
+
69
+ function detectPhysicalMac(networks, sysfsRoot) {
70
+ return physicalMacCandidates(networks, sysfsRoot)[0] || null;
71
+ }
72
+
73
+ function macDeviceId(mac) {
74
+ const normalized = normalizeMac(mac);
75
+ return normalized ? "ATELIER-MAC-" + normalized.replace(/:/g, "").toUpperCase() : null;
76
+ }
77
+
78
+ function readMacIdentity(networks, sysfsRoot) {
79
+ const override = normalizeMac(process.env.ATELIER_DEVICE_MAC);
80
+ if (override) return { mac: override, name: "environment" };
81
+ return detectPhysicalMac(networks, sysfsRoot);
82
+ }
83
+
84
+ function readVictronIdentity() {
85
+ const environmentId = String(process.env.VRM_PORTAL_ID || process.env.VICTRON_PORTAL_ID || "").trim();
86
+ if (environmentId) return { value: environmentId, source: "victron-environment" };
87
+ const directFiles = ["/data/conf/portal-id", "/data/conf/vrm-portal-id", "/data/conf/serial-number"];
88
+ for (const filePath of directFiles) {
89
+ const value = readText(filePath);
90
+ if (value) return { value, source: "victron:" + filePath };
91
+ }
92
+ const settingsXml = readText("/data/conf/settings.xml");
93
+ const match = settingsXml.match(/<(?:PortalId|UniqueIdentifier)>([^<]+)<\/(?:PortalId|UniqueIdentifier)>/i);
94
+ return match ? { value: match[1].trim(), source: "victron:/data/conf/settings.xml" } : null;
95
+ }
96
+
97
+ function stableDeviceId(value) {
98
+ return "ATELIER-" + crypto.createHash("sha256").update(String(value), "utf8").digest("hex").slice(0, 32).toUpperCase();
99
+ }
100
+
101
+ function localStableIdentities() {
102
+ const identities = [];
103
+ const victron = readVictronIdentity();
104
+ if (victron) identities.push({ deviceId: stableDeviceId(victron.value), source: victron.source });
105
+ ["/etc/machine-id", "/var/lib/dbus/machine-id", "/sys/class/dmi/id/product_uuid", "/proc/device-tree/serial-number"].forEach(function (source) {
106
+ const value = readText(source);
107
+ if (value) identities.push({ deviceId: stableDeviceId(value), source });
108
+ });
109
+ return identities;
110
+ }
111
+
112
+ function hostFingerprint() {
113
+ const identities = localStableIdentities();
114
+ return identities.length ? stableDeviceId(identities.map(function (identity) { return identity.deviceId; }).join("|")) : null;
115
+ }
116
+
117
+ function identityCachePath(userDir) {
118
+ return path.join(userDir, "atelier-license", IDENTITY_CACHE_FILE);
119
+ }
120
+
121
+ function readIdentityCache(userDir) {
122
+ try {
123
+ const cached = JSON.parse(fs.readFileSync(identityCachePath(userDir), "utf8"));
124
+ const mac = normalizeMac(cached && cached.mac);
125
+ if (!mac || cached.deviceId !== macDeviceId(mac)) return null;
126
+ const currentFingerprint = hostFingerprint();
127
+ if (cached.hostFingerprint && currentFingerprint && cached.hostFingerprint !== currentFingerprint) return null;
128
+ return { deviceId: cached.deviceId, mac, name: String(cached.interfaceName || "unknown") };
129
+ } catch (error) {
130
+ return null;
131
+ }
132
+ }
133
+
134
+ async function saveIdentityCache(userDir, candidate) {
135
+ const directory = path.join(userDir, "atelier-license");
136
+ await ensurePrivateDirectory(directory);
137
+ const target = identityCachePath(userDir);
138
+ const temporary = target + ".tmp-" + process.pid;
139
+ const body = JSON.stringify({
140
+ schemaVersion: 1,
141
+ deviceId: macDeviceId(candidate.mac),
142
+ mac: candidate.mac,
143
+ interfaceName: candidate.name,
144
+ hostFingerprint: hostFingerprint(),
145
+ savedAt: new Date().toISOString()
146
+ }, null, 2) + "\n";
147
+ await fs.promises.writeFile(temporary, body, { encoding: "utf8", mode: 0o600 });
148
+ try { await fs.promises.chmod(temporary, 0o600); } catch (error) { /* Windows/limited filesystems */ }
149
+ await fs.promises.rename(temporary, target);
150
+ try { await fs.promises.chmod(target, 0o600); } catch (error) { /* Windows/limited filesystems */ }
151
+ }
152
+
153
+ function wait(delayMs) {
154
+ return new Promise(function (resolve) { setTimeout(resolve, delayMs); });
155
+ }
156
+
157
+ function detectModel() {
158
+ return readText("/proc/device-tree/model") || readText("/sys/firmware/devicetree/base/model") || os.type() + " " + os.arch();
159
+ }
160
+
161
+ function detectVenusVersion() {
162
+ const version = readText("/opt/victronenergy/version") || readText("/etc/venus/version");
163
+ if (version) return version;
164
+ const osRelease = readText("/etc/os-release");
165
+ const match = osRelease.match(/^VERSION_ID=["']?([^"'\r\n]+)["']?/m);
166
+ return /venus/i.test(osRelease) && match ? match[1] : null;
167
+ }
168
+
169
+ function firstReadable(paths) {
170
+ for (const source of paths) {
171
+ const value = readText(source);
172
+ if (value && !/^unknown|not specified|none$/i.test(value)) return value;
173
+ }
174
+ return null;
175
+ }
176
+
177
+ function detectHardwareInfo() {
178
+ return {
179
+ hardwareSerial: firstReadable(["/proc/device-tree/serial-number", "/sys/firmware/devicetree/base/serial-number", "/sys/class/dmi/id/product_serial", "/data/conf/serial-number"]),
180
+ systemUuid: firstReadable(["/sys/class/dmi/id/product_uuid"]),
181
+ manufacturer: firstReadable(["/sys/class/dmi/id/sys_vendor", "/proc/device-tree/manufacturer", "/sys/firmware/devicetree/base/manufacturer"]),
182
+ osPlatform: os.platform(),
183
+ osRelease: os.release(),
184
+ cpuArch: os.arch()
185
+ };
186
+ }
187
+
188
+ function completeIdentity(identity, candidate, model, venusOsVersion) {
189
+ return Object.assign({}, identity, detectHardwareInfo(), {
190
+ deviceModel: model,
191
+ venusOsVersion,
192
+ macAddress: candidate && candidate.mac || null,
193
+ networkInterface: candidate && candidate.name || null,
194
+ identitySource: identity.source
195
+ });
196
+ }
197
+
198
+ async function getDeviceIdentity(userDir, options) {
199
+ const settings = options || {};
200
+ const preferredDeviceId = String(settings.preferredDeviceId || "").trim();
201
+ const model = detectModel();
202
+ const venusOsVersion = detectVenusVersion();
203
+ const override = normalizeMac(process.env.ATELIER_DEVICE_MAC);
204
+ if (override) return completeIdentity({ deviceId: macDeviceId(override), source: "mac:environment" }, { mac: override, name: "environment" }, model, venusOsVersion);
205
+
206
+ let candidates = physicalMacCandidates(settings.networks, settings.sysfsRoot);
207
+ const preferredMac = candidates.find(function (candidate) { return macDeviceId(candidate.mac) === preferredDeviceId; });
208
+ if (preferredMac) {
209
+ await saveIdentityCache(userDir, preferredMac);
210
+ return completeIdentity({ deviceId: preferredDeviceId, source: "mac:" + preferredMac.name }, preferredMac, model, venusOsVersion);
211
+ }
212
+
213
+ const stableIdentities = localStableIdentities();
214
+ const preferredStable = stableIdentities.find(function (identity) { return identity.deviceId === preferredDeviceId; });
215
+ if (preferredStable) return completeIdentity({ deviceId: preferredStable.deviceId, source: preferredStable.source }, candidates[0], model, venusOsVersion);
216
+
217
+ const cached = readIdentityCache(userDir);
218
+ if (cached) return completeIdentity({ deviceId: cached.deviceId, source: "cached-mac:" + cached.name }, { mac: cached.mac, name: cached.name }, model, venusOsVersion);
219
+
220
+ const retryDelays = Array.isArray(settings.macRetryDelaysMs) ? settings.macRetryDelaysMs : DEFAULT_MAC_RETRY_DELAYS_MS;
221
+ for (const delayMs of retryDelays) {
222
+ if (candidates.length) break;
223
+ if (delayMs > 0) await wait(delayMs);
224
+ candidates = physicalMacCandidates(settings.networks, settings.sysfsRoot);
225
+ }
226
+ if (candidates.length) {
227
+ await saveIdentityCache(userDir, candidates[0]);
228
+ return completeIdentity({ deviceId: macDeviceId(candidates[0].mac), source: "mac:" + candidates[0].name }, candidates[0], model, venusOsVersion);
229
+ }
230
+
231
+ if (stableIdentities.length) return completeIdentity({ deviceId: stableIdentities[0].deviceId, source: stableIdentities[0].source }, null, model, venusOsVersion);
232
+
233
+ const directory = path.join(userDir, "atelier-license");
234
+ const fallbackPath = path.join(directory, "device-id");
235
+ const existing = readText(fallbackPath);
236
+ if (existing) return completeIdentity({ deviceId: existing, source: "persistent-fallback" }, null, model, venusOsVersion);
237
+
238
+ await ensurePrivateDirectory(directory);
239
+ const generated = stableDeviceId(crypto.randomUUID());
240
+ await fs.promises.writeFile(fallbackPath, generated + "\n", { encoding: "utf8", mode: 0o600, flag: "wx" }).catch(async function (error) {
241
+ if (error.code !== "EEXIST") throw error;
242
+ });
243
+ try { await fs.promises.chmod(fallbackPath, 0o600); } catch (error) { /* Windows/limited filesystems */ }
244
+ return completeIdentity({ deviceId: readText(fallbackPath) || generated, source: "persistent-fallback" }, null, model, venusOsVersion);
245
+ }
246
+
247
+ async function getDeviceId(userDir) {
248
+ return (await getDeviceIdentity(userDir)).deviceId;
249
+ }
250
+
251
+ module.exports = { detectHardwareInfo, detectPhysicalMac, getDeviceId, getDeviceIdentity, macDeviceId, normalizeMac, physicalMacCandidates, readIdentityCache, stableDeviceId };
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+
3
+ class LicenseError extends Error {
4
+ constructor(code, message, details) {
5
+ super(message || code);
6
+ this.name = "LicenseError";
7
+ this.code = code;
8
+ this.details = details || null;
9
+ }
10
+ }
11
+
12
+ class LicenseNetworkError extends LicenseError {
13
+ constructor(message, cause) {
14
+ super("NETWORK_ERROR", message || "License server unavailable");
15
+ this.name = "LicenseNetworkError";
16
+ this.cause = cause;
17
+ this.isNetworkError = true;
18
+ }
19
+ }
20
+
21
+ module.exports = { LicenseError, LicenseNetworkError };