Skip to content

Commit 0ce3996

Browse files
authored
feat: read schedule,calendar and trend (#64)
## Summary This PR ports schedule/calendar/trend read compatibility from the legacy `node-bacstack` implementation into `@bacnet-js/client`, and adds read examples for manual validation. Source reference: - https://github.com/EveGun/node-bacstack ## What changed - Added specialized decode paths in `ReadProperty.decodeAcknowledge` for: - `WEEKLY_SCHEDULE` - `EXCEPTION_SCHEDULE` - `EFFECTIVE_PERIOD` - `DATE_LIST` - Added BACnet `WEEKNDAY` support in ASN.1 decode flow. - Added `decodeRange` integration in `ReadRange.decodeAcknowledge`. - Fixed non-zero offset slicing in `ReadRange` fallback range buffer handling. - Fixed closing-tag length consumption in exception schedule decode. - Added unit tests for schedule/calendar parsing and trend range decoding. - Added read examples: - `examples/read-schedule-weekly.ts` - `examples/read-schedule-exceptions.ts` - `examples/read-schedule-period.ts` - `examples/read-calendar-datelist.ts` - `examples/read-trend.ts` ## Type consistency - Trend range `timestamp` now returns `Date` (instead of epoch number) for consistency with other decoded date/time fields. ## Validation - `npm run lint` ✅ - `npm run test:unit` ✅ - Docker-based compliance tests executed successfully in local environment ✅ - Manually validated against a WAGO PFC200 device (schedule/calendar/trend read paths) ✅ ## Notes - Write examples were intentionally removed from this read-focused branch. - Write-side porting will follow in a separate branch/PR. - Parser behavior was validated against real BACnet traffic captures from WAGO PFC200 communication (Wireshark), then confirmed with live device reads.
1 parent 9e3c234 commit 0ce3996

12 files changed

Lines changed: 1188 additions & 15 deletions

examples/read-calendar-datelist.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import Bacnet, { ObjectType, PropertyIdentifier } from '../src'
2+
3+
const target = process.argv[2] || '192.168.40.245'
4+
const instance = Number(process.argv[3] || 0)
5+
const port = Number(process.argv[4] || 47808)
6+
const address = { address: `${target}:${port}` }
7+
8+
const client = new Bacnet({ apduTimeout: 4000 })
9+
10+
client.on('error', (err: Error) => {
11+
console.error(err)
12+
client.close()
13+
})
14+
15+
async function main() {
16+
try {
17+
const value = await client.readProperty(
18+
address,
19+
{ type: ObjectType.CALENDAR, instance },
20+
PropertyIdentifier.DATE_LIST,
21+
)
22+
const entries = (value.values[0]?.value as any[]) || []
23+
entries.forEach((entry) => console.log(entry))
24+
} catch (err) {
25+
console.error(err)
26+
} finally {
27+
client.close()
28+
}
29+
}
30+
31+
void main()
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import Bacnet, { ObjectType, PropertyIdentifier } from '../src'
2+
3+
const target = process.argv[2] || '192.168.40.245'
4+
const instance = Number(process.argv[3] || 0)
5+
const port = Number(process.argv[4] || 47808)
6+
const address = { address: `${target}:${port}` }
7+
8+
const client = new Bacnet({ apduTimeout: 4000 })
9+
10+
client.on('error', (err: Error) => {
11+
console.error(err)
12+
client.close()
13+
})
14+
15+
async function main() {
16+
try {
17+
const value = await client.readProperty(
18+
address,
19+
{ type: ObjectType.SCHEDULE, instance },
20+
PropertyIdentifier.EXCEPTION_SCHEDULE,
21+
)
22+
const specialEvents = (value.values[0]?.value as any[]) || []
23+
specialEvents.forEach((entry) => console.log(entry))
24+
} catch (err) {
25+
console.error(err)
26+
} finally {
27+
client.close()
28+
}
29+
}
30+
31+
void main()

examples/read-schedule-period.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import Bacnet, { ObjectType, PropertyIdentifier } from '../src'
2+
3+
const target = process.argv[2] || '192.168.40.245'
4+
const instance = Number(process.argv[3] || 0)
5+
const port = Number(process.argv[4] || 47808)
6+
const address = { address: `${target}:${port}` }
7+
8+
const client = new Bacnet({ apduTimeout: 4000 })
9+
10+
client.on('error', (err: Error) => {
11+
console.error(err)
12+
client.close()
13+
})
14+
15+
async function main() {
16+
try {
17+
const value = await client.readProperty(
18+
address,
19+
{ type: ObjectType.SCHEDULE, instance },
20+
PropertyIdentifier.EFFECTIVE_PERIOD,
21+
)
22+
console.log(value.values[0]?.value || [])
23+
} catch (err) {
24+
console.error(err)
25+
} finally {
26+
client.close()
27+
}
28+
}
29+
30+
void main()

examples/read-schedule-weekly.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import Bacnet, { ObjectType, PropertyIdentifier } from '../src'
2+
3+
const target = process.argv[2] || '192.168.40.245:47808'
4+
const instance = Number(process.argv[3] || 0)
5+
const localPortArg = process.argv[4]
6+
const address = {
7+
address: target.includes(':') ? target : `${target}:47808`,
8+
}
9+
10+
const client = new Bacnet(
11+
localPortArg
12+
? { apduTimeout: 4000, port: Number(localPortArg) }
13+
: { apduTimeout: 4000 },
14+
)
15+
16+
client.on('error', (err: Error) => {
17+
console.error(err)
18+
client.close()
19+
})
20+
21+
async function main() {
22+
try {
23+
const value = await client.readProperty(
24+
address,
25+
{ type: ObjectType.SCHEDULE, instance },
26+
PropertyIdentifier.WEEKLY_SCHEDULE,
27+
)
28+
const weekly = (value.values[0]?.value as any[]) || []
29+
weekly.forEach((day, index) => {
30+
console.log(`day ${index}:`, day)
31+
})
32+
} catch (err) {
33+
console.error(err)
34+
} finally {
35+
client.close()
36+
}
37+
}
38+
39+
void main()

examples/read-trend.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import Bacnet, {
2+
MaxSegmentsAccepted,
3+
ObjectType,
4+
PropertyIdentifier,
5+
} from '../src'
6+
7+
const target = process.argv[2] || '192.168.40.245'
8+
const instance = Number(process.argv[3] || 0)
9+
const startIndex = Number(process.argv[4] || 1)
10+
const count = Number(process.argv[5] || 55)
11+
const port = Number(process.argv[6] || 47808)
12+
const address = { address: `${target}:${port}` }
13+
14+
const client = new Bacnet({ apduTimeout: 10000 })
15+
16+
client.on('error', (err: Error) => {
17+
console.error(err)
18+
client.close()
19+
})
20+
21+
async function main() {
22+
try {
23+
const recordCount = await client.readProperty(
24+
address,
25+
{ type: ObjectType.TREND_LOG, instance },
26+
PropertyIdentifier.RECORD_COUNT,
27+
)
28+
console.log('recordCount:', recordCount.values?.[0]?.value)
29+
30+
const response = await client.readRange(
31+
address,
32+
{ type: ObjectType.TREND_LOG, instance },
33+
startIndex,
34+
count,
35+
{ maxSegments: MaxSegmentsAccepted.SEGMENTS_65 },
36+
)
37+
38+
// Until readRange typed trend decoding is ported, inspect raw payload.
39+
if ((response as any).values) {
40+
console.log((response as any).values)
41+
} else {
42+
console.log(response)
43+
console.log(response.rangeBuffer)
44+
}
45+
} catch (err) {
46+
console.error(err)
47+
} finally {
48+
client.close()
49+
}
50+
}
51+
52+
void main()

0 commit comments

Comments
 (0)