From 93a8a631e3a7d566773d1f4fe019afbb952b985c Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 12 Oct 2026 19:54:20 +0000 Subject: [PATCH] Post-0.9.0 cleanup: RPM spec equality, hide internal helper, fix docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - BacnetReadAccessSpecification == and hashCode compared only properties.length, so two specifications for the same object requesting different properties compared equal and collided in a Set/Map. Compare the property list by content, and add a regression test. - Stop exporting the internal mapConcurrent fan-out helper from the public API; it was unintentionally re-exported through device_scanner.dart. - README: correct the stale claim that the server does not implement SubscribeCOVPropertyMultiple — it has as a target since 0.9.0. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01Q3Woz4Bu11Eqhjsh6q3q5b --- CHANGELOG.md | 14 +++++++++++++ README.md | 4 ++-- lib/bacnet_plugin.dart | 3 ++- lib/src/models/rpm_models.dart | 14 +++++++++---- test/models/rpm_models_test.dart | 35 ++++++++++++++++++++++++++++++++ 5 files changed, 63 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a504d6..7b529a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Fixed + +- `BacnetReadAccessSpecification` equality and `hashCode` compared only the + number of properties, so two specifications for the same object that + requested different properties compared equal and collided in a `Set` or + `Map`. They now compare the property list by content. + +### Changed + +- The internal `mapConcurrent` fan-out helper is no longer exported from the + public API; it was unintentionally re-exported through `device_scanner.dart`. + ## [0.9.0] - 2026-10-12 ### Added diff --git a/README.md b/README.md index 7738700..ead02f6 100644 --- a/README.md +++ b/README.md @@ -962,8 +962,8 @@ client.covEvents.listen((event) { ``` Devices without the service reject it (`BacnetRejectException`); use -`subscribeCOV` for them. bacnet-stack, and so the server of this package, -does not implement it. +`subscribeCOV` for them. The server of this package implements +SubscribeCOVPropertyMultiple as a target (since 0.9.0). ## Device descriptions and the command line tool diff --git a/lib/bacnet_plugin.dart b/lib/bacnet_plugin.dart index 5754717..955c338 100644 --- a/lib/bacnet_plugin.dart +++ b/lib/bacnet_plugin.dart @@ -60,7 +60,8 @@ export 'src/server/server_state.dart'; export 'src/utilities/alarm_subscription.dart'; export 'src/utilities/device_backup.dart'; export 'src/utilities/device_description.dart'; -export 'src/utilities/device_scanner.dart'; +// mapConcurrent is an internal fan-out helper, not part of the public API. +export 'src/utilities/device_scanner.dart' hide mapConcurrent; export 'src/utilities/file_transfer.dart'; export 'src/utilities/network_discovery.dart'; export 'src/utilities/property_monitor.dart'; diff --git a/lib/src/models/rpm_models.dart b/lib/src/models/rpm_models.dart index c44215f..0346ff1 100644 --- a/lib/src/models/rpm_models.dart +++ b/lib/src/models/rpm_models.dart @@ -117,13 +117,19 @@ class BacnetReadAccessSpecification { @override bool operator ==(Object other) { if (identical(this, other)) return true; - return other is BacnetReadAccessSpecification && - other.objectIdentifier == objectIdentifier && - other.properties.length == properties.length; + if (other is! BacnetReadAccessSpecification || + other.objectIdentifier != objectIdentifier || + other.properties.length != properties.length) { + return false; + } + for (var i = 0; i < properties.length; i++) { + if (other.properties[i] != properties[i]) return false; + } + return true; } @override - int get hashCode => Object.hash(objectIdentifier, properties.length); + int get hashCode => Object.hash(objectIdentifier, Object.hashAll(properties)); @override String toString() => '$objectIdentifier: $properties'; diff --git a/test/models/rpm_models_test.dart b/test/models/rpm_models_test.dart index 1877171..bd92cc3 100644 --- a/test/models/rpm_models_test.dart +++ b/test/models/rpm_models_test.dart @@ -180,6 +180,41 @@ void main() { expect(spec1, isNot(equals(spec3))); }); + test('equality compares property contents, not just count', () { + const obj = BacnetObject(type: BacnetObjectType.analogInput, instance: 1); + const presentValue = BacnetReadAccessSpecification( + objectIdentifier: obj, + properties: [ + BacnetPropertyReference( + propertyIdentifier: BacnetPropertyId.presentValue, + ), + ], + ); + const samePresentValue = BacnetReadAccessSpecification( + objectIdentifier: obj, + properties: [ + BacnetPropertyReference( + propertyIdentifier: BacnetPropertyId.presentValue, + ), + ], + ); + // same object and same property count, but a different property + const statusFlags = BacnetReadAccessSpecification( + objectIdentifier: obj, + properties: [ + BacnetPropertyReference( + propertyIdentifier: BacnetPropertyId.statusFlags, + ), + ], + ); + + expect(presentValue, equals(samePresentValue)); + expect(presentValue.hashCode, equals(samePresentValue.hashCode)); + expect(presentValue, isNot(equals(statusFlags))); + // distinct specs must not collapse in a hash set + expect({presentValue, statusFlags}, hasLength(2)); + }); + group('JSON serialization', () { test('toJson serializes nested objects', () { const obj = BacnetObject(