From f81b7b4365a25e8c0af220fd2596d086d23aef31 Mon Sep 17 00:00:00 2001 From: rakaustin <237724534+rakaustin@users.noreply.github.com> Date: Mon, 14 Sep 2026 03:58:59 +0000 Subject: [PATCH 1/4] Add BACnet profile for issue --- .../Eddy-Solutions/Eddy-Solutions-H20-V3.yaml | 150 ++++++++++++++++++ .../tests/Eddy-Solutions-H20-V3.test.json | 87 ++++++++++ registry.json | 24 ++- 3 files changed, 257 insertions(+), 4 deletions(-) create mode 100644 profiles/Eddy-Solutions/Eddy-Solutions-H20-V3.yaml create mode 100644 profiles/Eddy-Solutions/tests/Eddy-Solutions-H20-V3.test.json diff --git a/profiles/Eddy-Solutions/Eddy-Solutions-H20-V3.yaml b/profiles/Eddy-Solutions/Eddy-Solutions-H20-V3.yaml new file mode 100644 index 0000000..ca0632b --- /dev/null +++ b/profiles/Eddy-Solutions/Eddy-Solutions-H20-V3.yaml @@ -0,0 +1,150 @@ +codec: | + function validBytes(data) { + if (!Array.isArray(data) || data.length === 0 || data.length > 255) { + return "invalid payload"; + } + for (var i = 0; i < data.length; i += 1) { + if (!Number.isInteger(data[i]) || data[i] < 0 || data[i] > 255) { + return "invalid payload byte"; + } + } + return null; + } + + function frameError(fPort, data) { + if (fPort !== 3) { + return "unsupported fPort"; + } + var error = validBytes(data); + if (error !== null) { + return error; + } + var type = data[0]; + if (type === 0x00 || type === 0x03) { + return data.length === 11 ? null : "invalid describe/ack length"; + } + if (type === 0x01) { + return data.length === 11 ? null : "invalid report length"; + } + if (type === 0x02 || type === 0x04) { + return data.length === 7 ? null : "invalid alert/deep-sleep length"; + } + return "unsupported message type"; + } + + function appendStatus(values, status) { + values.push({ name: "Leak Alert", channel: 1, value: status & 0x01, unit: null }); + values.push({ name: "Leak Clear", channel: 2, value: (status >> 3) & 0x01, unit: null }); + } + + function Decode(fPort, data, variables) { + var error = frameError(fPort, data); + if (error !== null) { + return []; + } + + var values = []; + var type = data[0]; + appendStatus(values, data[1]); + + if (type === 0x00) { + values.push({ name: "Get Report Period", channel: 5, value: data[2], unit: null }); + // Firmware byte: bits 5-7 major, bits 2-4 minor, bits 0-1 revision. + values.push({ name: "Firmware Version", channel: 6, value: data[10], unit: null }); + values.push({ name: "Set Report Period", channel: 7, value: data[2], unit: null }); + return values; + } + + if (type === 0x01 || type === 0x02 || type === 0x04) { + values.push({ name: "Temperature", channel: 3, value: data[2], unit: "degreesCelsius" }); + values.push({ name: "Humidity", channel: 4, value: data[3], unit: "percentRelativeHumidity" }); + return values; + } + + if (type === 0x03) { + values.push({ name: "Get Report Period", channel: 5, value: data[2], unit: null }); + values.push({ name: "Set Report Period", channel: 7, value: data[2], unit: null }); + return values; + } + + return []; + } + + function Encode(data, variables) { + if (!data || !Number.isInteger(data.channel) || !Number.isFinite(data.value)) { + return []; + } + if (data.channel !== 7 || !Number.isInteger(data.value) || data.value < 0 || data.value > 255) { + return []; + } + return [0x10, data.value, 0x0F]; + } + + function decodeUplink(input) { + if (!input) { + return { data: [], errors: ["invalid input"] }; + } + var error = frameError(input.fPort, input.bytes); + if (error !== null) { + return { data: [], errors: [error] }; + } + return { data: Decode(input.fPort, input.bytes, input.variables) }; + } + + function encodeDownlink(input) { + if (!input) { + return { bytes: [], errors: ["invalid input"] }; + } + var bytes = Encode(input.data, input.variables); + if (bytes.length === 0) { + return { bytes: [], errors: ["unsupported channel or value"] }; + } + return { bytes: bytes }; + } + +datatype: + "1": + name: Leak Alert + type: BinaryInputObject + channel: 1 + "2": + name: Leak Clear + type: BinaryInputObject + channel: 2 + "3": + name: Temperature + type: AnalogValueObject + units: degreesCelsius + channel: 3 + "4": + name: Humidity + type: AnalogValueObject + units: percentRelativeHumidity + channel: 4 + "5": + name: Get Report Period + type: AnalogValueObject + channel: 5 + "6": + name: Firmware Version + type: AnalogValueObject + channel: 6 + "7": + name: Set Report Period + type: AnalogValueObject + fport: 5 + channel: 7 + +lorawan: + adrAlgorithm: LoRa Only + classCDownlinkTimeout: 5 + macVersion: LORAWAN_1_0_3 + regionalParametersRevision: A + supportClassB: false + supportClassC: false + supportOTAA: true +model: Eddy-Solutions-H20-V3 +profileVersion: 1.0.0 +name: H20-V3 +vendor: Eddy-Solutions +id: f2c5f7c1-6b5d-4b7a-9f1e-2c4d8a6e7310 diff --git a/profiles/Eddy-Solutions/tests/Eddy-Solutions-H20-V3.test.json b/profiles/Eddy-Solutions/tests/Eddy-Solutions-H20-V3.test.json new file mode 100644 index 0000000..dab91c7 --- /dev/null +++ b/profiles/Eddy-Solutions/tests/Eddy-Solutions-H20-V3.test.json @@ -0,0 +1,87 @@ +{ + "schemaVersion": 1, + "strict": true, + "profile": "Eddy-Solutions-H20-V3", + "evidenceLevel": "documentation-only", + "reviewMode": "single-model", + "fPortPolicy": { + "mode": "fixed", + "ports": [3], + "citation": "Issue #41 supplies all uplink examples on fPort 3; the documented Set Report and Read Time Period downlink is on fPort 5." + }, + "sources": [ + { + "type": "issue", + "reference": "https://github.com/RAKWireless/RAK-BACnet-Profiles/issues/41", + "citation": "Four known uplink payloads, BACnet mappings, and the Set Report Period downlink vector." + }, + { + "type": "official-document", + "reference": "H20 Leak Detector V3 Technical Specification, supplied official-document.txt", + "citation": "Sections 5.1 and 6.1-6.6; 9-page PDF SHA-256 cc69eaeb11bea043d32f2f92f652a8bb8d1a890c435ee0cc117ee373c5d7e768." + } + ], + "robustness": { + "checkTruncation": true, + "checkUnknownFPort": true, + "checkFuzz": true + }, + "testCases": [ + { + "name": "Describe", + "fPort": 3, + "input": "0000040F27E3286E000097", + "expectedOutput": [ + { "name": "Leak Alert", "channel": 1, "value": 0, "unit": null }, + { "name": "Leak Clear", "channel": 2, "value": 0, "unit": null }, + { "name": "Get Report Period", "channel": 5, "value": 4, "unit": null }, + { "name": "Firmware Version", "channel": 6, "value": 151, "unit": null }, + { "name": "Set Report Period", "channel": 7, "value": 4, "unit": null } + ] + }, + { + "name": "Report", + "fPort": 3, + "input": "010015241E640A15162424", + "expectedOutput": [ + { "name": "Leak Alert", "channel": 1, "value": 0, "unit": null }, + { "name": "Leak Clear", "channel": 2, "value": 0, "unit": null }, + { "name": "Temperature", "channel": 3, "value": 21, "unit": "degreesCelsius" }, + { "name": "Humidity", "channel": 4, "value": 36, "unit": "percentRelativeHumidity" } + ] + }, + { + "name": "Alert", + "fPort": 3, + "input": "0201185A215A08", + "expectedOutput": [ + { "name": "Leak Alert", "channel": 1, "value": 1, "unit": null }, + { "name": "Leak Clear", "channel": 2, "value": 0, "unit": null }, + { "name": "Temperature", "channel": 3, "value": 24, "unit": "degreesCelsius" }, + { "name": "Humidity", "channel": 4, "value": 90, "unit": "percentRelativeHumidity" } + ] + }, + { + "name": "Acknowledgement", + "fPort": 3, + "input": "0318040F27FE286E000097", + "expectedOutput": [ + { "name": "Leak Alert", "channel": 1, "value": 0, "unit": null }, + { "name": "Leak Clear", "channel": 2, "value": 1, "unit": null }, + { "name": "Get Report Period", "channel": 5, "value": 4, "unit": null }, + { "name": "Set Report Period", "channel": 7, "value": 4, "unit": null } + ] + } + ], + "downlinkTestCases": [ + { + "name": "Set Report Period to 20 Hours", + "channel": 7, + "value": 20, + "expectedFPort": 5, + "expectedBytes": "10 14 0F", + "description": "Issue #41 prepared downlink evidence: report period 20 hours and read period 15 minutes.", + "citation": "Issue #41 downlinkEvidenceText." + } + ] +} diff --git a/registry.json b/registry.json index bacc337..155ea6e 100644 --- a/registry.json +++ b/registry.json @@ -1,8 +1,8 @@ { "$schema": "./registry-schema.json", "version": "1.0.0", - "lastUpdate": "2026-08-12", - "totalProfiles": 27, + "lastUpdate": "2026-09-14", + "totalProfiles": 28, "profiles": [ { "id": "rakwireless-rak2461", @@ -148,6 +148,22 @@ "contentSha256": "ec6bd00b66f02bbc08dc5b9f9a929d0622355b89ee95e8bca4a2ce817c853c65", "lastUpdate": "2026-04-24" }, + { + "id": "eddy-solutions-h20-v3", + "vendor": "Eddy-Solutions", + "model": "Eddy-Solutions-H20-V3", + "version": "3.0.0", + "path": "profiles/Eddy-Solutions/Eddy-Solutions-H20-V3.yaml", + "verified": false, + "hasTests": true, + "description": "Eddy-Solutions Eddy-Solutions-H20-V3 Sensor", + "deviceType": "Sensor", + "lorawanClass": [ + "A" + ], + "contentSha256": "1459359acd021ba0cea89306c932dd77fbdaddd08cdd1d1437eb743bcb83bc66", + "lastUpdate": "2026-09-14" + }, { "id": "eddy-solutions-lora-iq-v2", "vendor": "Eddy-Solutions", @@ -442,7 +458,7 @@ "RAKwireless": 2, "Becasmart": 1, "Dragino": 6, - "Eddy-Solutions": 1, + "Eddy-Solutions": 2, "Kaiterra": 1, "Milesight": 4, "Moko": 1, @@ -450,7 +466,7 @@ "Senso8": 9, "Thermokon": 1 }, - "withTests": 5, + "withTests": 6, "withoutTests": 22 } } From f37871e4315f521d1b94c580dc1e0823b92c0e34 Mon Sep 17 00:00:00 2001 From: rakaustin Date: Mon, 14 Sep 2026 12:29:55 +0800 Subject: [PATCH 2/4] Update registry.json --- registry.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/registry.json b/registry.json index 155ea6e..46f8fc9 100644 --- a/registry.json +++ b/registry.json @@ -159,7 +159,7 @@ "description": "Eddy-Solutions Eddy-Solutions-H20-V3 Sensor", "deviceType": "Sensor", "lorawanClass": [ - "A" + "C" ], "contentSha256": "1459359acd021ba0cea89306c932dd77fbdaddd08cdd1d1437eb743bcb83bc66", "lastUpdate": "2026-09-14" From c631716b511a0bdffb095a17aca4502e37ee018b Mon Sep 17 00:00:00 2001 From: rakaustin Date: Mon, 14 Sep 2026 12:38:08 +0800 Subject: [PATCH 3/4] Update registry.json --- registry.json | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/registry.json b/registry.json index 604563e..4396f21 100644 --- a/registry.json +++ b/registry.json @@ -2,7 +2,7 @@ "$schema": "./registry-schema.json", "version": "1.0.0", "lastUpdate": "2026-09-14", - "totalProfiles": 28, + "totalProfiles": 29, "profiles": [ { "id": "rakwireless-rak2461", @@ -164,6 +164,22 @@ "contentSha256": "1459359acd021ba0cea89306c932dd77fbdaddd08cdd1d1437eb743bcb83bc66", "lastUpdate": "2026-09-14" }, + { + "id": "eddy-solutions-link", + "vendor": "Eddy-Solutions", + "model": "Eddy-Solutions-Link", + "version": "1.0.0", + "path": "profiles/Eddy-Solutions/Eddy-Solutions-Link.yaml", + "verified": false, + "hasTests": true, + "description": "Eddy-Solutions Eddy-Solutions-Link Sensor", + "deviceType": "Sensor", + "lorawanClass": [ + "C" + ], + "contentSha256": "63240ba3d632dcad40a9809d613af9aee70161612c7a8d90134e97e2a19f6ef6", + "lastUpdate": "2026-09-14" + }, { "id": "eddy-solutions-lora-iq-v2", "vendor": "Eddy-Solutions", @@ -458,7 +474,7 @@ "RAKwireless": 2, "Becasmart": 1, "Dragino": 6, - "Eddy-Solutions": 2, + "Eddy-Solutions": 3, "Kaiterra": 1, "Milesight": 4, "Moko": 1, @@ -466,7 +482,7 @@ "Senso8": 9, "Thermokon": 1 }, - "withTests": 6, + "withTests": 7, "withoutTests": 22 } } From cb936d1bed3ae37da0c93c3b14c6970766f2ca38 Mon Sep 17 00:00:00 2001 From: rakaustin Date: Mon, 14 Sep 2026 12:43:21 +0800 Subject: [PATCH 4/4] Update registry.json --- registry.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/registry.json b/registry.json index 4396f21..085672c 100644 --- a/registry.json +++ b/registry.json @@ -159,7 +159,7 @@ "description": "Eddy-Solutions Eddy-Solutions-H20-V3 Sensor", "deviceType": "Sensor", "lorawanClass": [ - "C" + "A" ], "contentSha256": "1459359acd021ba0cea89306c932dd77fbdaddd08cdd1d1437eb743bcb83bc66", "lastUpdate": "2026-09-14"