From c01225cc06e53adf17bf570a619c9d84a2ec83ea Mon Sep 17 00:00:00 2001 From: Khyati Mahendru Date: Mon, 19 Oct 2026 07:47:16 +0000 Subject: [PATCH 1/7] feat(discovery): implement Site Model Discovery in UDMIS, UUFI, and Butler --- .gencode_hash.txt | 18 +- bin/gencode_py | 44 + bin/gencode_root_schemas | 3 +- bin/gendocs.py | 28 + bin/test_butler | 8 +- bin/test_uufi_client | 52 +- butler/src/connection.py | 9 +- butler/src/discovery.py | 32 +- butler/src/handlers/discovery.py | 53 +- butler/src/mapping.py | 14 +- butler/tests/test_handlers.py | 109 + docs/specs/discovery.md | 11 + docs/specs/uufi.md | 2 +- gencode/docs/config.html | 55 + gencode/docs/events.html | 27040 ++++++++ gencode/docs/events_discovery.html | 51074 ++++++++++++++++ gencode/docs/metadata.html | 90 + gencode/java/udmi/schema/DiscoveryEvents.java | 20 +- .../udmi/schema/FamilyDiscoveryConfig.java | 10 +- .../udmi/schema/config_discovery_family.py | 1 + .../python/udmi/schema/events_discovery.py | 4 + gencode/python/udmi/schema/metadata.py | 3 +- mcp/butler/client.py | 4 + mcp/butler/provider.py | 84 + mcp/butler/server.py | 27 + mcp/uufi/provider.py | 2 + schema/category.json | 1 - schema/config_discovery_family.json | 4 + schema/events_discovery.json | 9 + tests/schemas/config/model_discovery.json | 13 + tests/schemas/config/model_discovery.out | 0 .../model_discovery_device.json | 33 + .../model_discovery_device.out | 0 .../model_discovery_start.json | 8 + .../model_discovery_start.out | 0 .../udmi/service/core/ControlProcessor.java | 10 + .../service/core/ModelDiscoveryHandler.java | 207 + .../udmi/service/core/ReflectProcessor.java | 29 +- .../udmi/service/core/TargetProcessor.java | 5 + .../bos/udmi/service/core/UufiProcessor.java | 28 +- .../messaging/impl/SimpleMqttPipe.java | 10 +- .../service/core/ReflectProcessorTest.java | 12 + .../udmi/service/core/UufiProcessorTest.java | 149 + .../messaging/impl/SimpleMqttPipeTest.java | 15 + .../google/daq/mqtt/registrar/Registrar.java | 2 +- 45 files changed, 79280 insertions(+), 52 deletions(-) create mode 100644 tests/schemas/config/model_discovery.json create mode 100644 tests/schemas/config/model_discovery.out create mode 100644 tests/schemas/events_discovery/model_discovery_device.json create mode 100644 tests/schemas/events_discovery/model_discovery_device.out create mode 100644 tests/schemas/events_discovery/model_discovery_start.json create mode 100644 tests/schemas/events_discovery/model_discovery_start.out create mode 100644 udmis/src/main/java/com/google/bos/udmi/service/core/ModelDiscoveryHandler.java diff --git a/.gencode_hash.txt b/.gencode_hash.txt index 436bbf4596..0ba4f3f7b4 100644 --- a/.gencode_hash.txt +++ b/.gencode_hash.txt @@ -1,21 +1,21 @@ 23553972c4b777768f0f70fc0f89a0ec81f580b64031169a96379ea22bb95e79 gencode/docs/building_config_entity.html c21266e8de23b6e8acfac16a177db782375bf71df470c343c3f8eb8b822ef56a gencode/docs/commands_discovery.html -8aa5b5f48f97771deaf5585f8ee74c605c2a43568af04996ff8439843274cce9 gencode/docs/config.html +18c7a15e09ec8de5c7508e077c4baf60b9c9abb4f1919551d2c1a93042a40348 gencode/docs/config.html 0dcc1e8245bf0b2791a4bf9be15d8647acd89124ade475345ce4a9c56d62a824 gencode/docs/configuration_endpoint.html 40f078753386e07bfc200eaf3c35cb4362eb413101f4f00d87c88cde3ee53080 gencode/docs/configuration_execution.html 0e2f9b56fd0d3762ff7ce14e0f9fc119cba17a3c2eeb2dcdbf1ff2fad5c6c9d1 gencode/docs/configuration_pod.html 94bb566ac6a4b6b7f89cd2cf1911f658446cc0bea405014275104623f9964dd3 gencode/docs/configuration_pubber.html 1057fa40fb7a31a23bb2773d21c38cf4590a935bd8b5ea4218e695c6204f5dd9 gencode/docs/data_template.html 1d5294b4a1fea6555e23144e594523e4793c795069b6880ce607ad09a6015581 gencode/docs/equipment_translation.html -cc29a77c12a615e23d070c4bd6337c32f0fa057efa12df771a78c4be465aac9e gencode/docs/events.html +248dd071e4981e17dfe2e2a455e21c43ba179fa1141d00be33ded36838bcd9f3 gencode/docs/events.html 70e57ad6ef39330d958727ebf9dcd61ef6ea30e4c8653eac412bf1867fdb3a70 gencode/docs/events_alarmset.html -2734f2b1fd0d22cdc2381d728376b031aecc4a3653a1faf5f743b973d43872dc gencode/docs/events_discovery.html +59cc1e456b56b7cccf41c5f87b2632cec0a69b021f871145fa56d0e4528196ee gencode/docs/events_discovery.html eafcc3c48189f605f114cde051fea9d13fc5f1a3e395d64fb0a91cb53d4c9aeb gencode/docs/events_pointset.html b65ce6e8061e61ba676a72c624f1c425bd238e152ce8e3ebb1e9c23f0e502bcb gencode/docs/events_streams.html 81043ba732caab75704d38e6977b32b1c2bcdad89d94ad2a259d0e70ca4aa40e gencode/docs/events_system.html 46fd37b577c655965346d1e3ececf559c4384791214900eeeea1ad8fc72a02b8 gencode/docs/events_udmi.html baf59590b1a71a49e59d8f499ae5112a1b7a555294dbd72bc821203aecceefa3 gencode/docs/events_validation.html -8d77357337531f7df7b88c332d9cac934723095c5be45ca410e31c9d1f665ee1 gencode/docs/metadata.html +a4feddcbec98f08c316b4dd529e28b588dd255bf4fc15023f0a631d99a77bc6f gencode/docs/metadata.html 18178a591940c176371fb9196048eaacd40f1323997cf907ca11eb879a095c7e gencode/docs/monitoring.html 64d21cf916e20566171cf9d723e59c297b576bcba89e2a73aec2b018398cc7da gencode/docs/mosquitto_client_response.html 6f332da9cc64230df3551f9748a4eaa4060146e767cae967fc0e73b1f348be5b gencode/docs/persistent_device.html @@ -66,7 +66,7 @@ c04302f973508295258d4d3a29b5da70bf3cd3a6e2c63a958fb70dd3d370276c gencode/java/u 52ae2afb2dcb7863259e7eea71888811663c010429c09760493f5f006a660c84 gencode/java/udmi/schema/Dimension.java 2bb1513817eea9c0486b28f3316ead16d53885a8bad9f6a42ee3d5f762335a68 gencode/java/udmi/schema/DiscoveryCommand.java a572f17a61584a32ff19968356f7f7d7bcc8430131ac3701878d0bd221e1a1fa gencode/java/udmi/schema/DiscoveryConfig.java -9217b2c963003ca51ec61b9aadd40877634f2bea08d7fd412b51521e8ec61cfb gencode/java/udmi/schema/DiscoveryEvents.java +ebc57084db00ed858c8bebcf926efc39c18ff345d9b5ca963bf52118635e6832 gencode/java/udmi/schema/DiscoveryEvents.java af16026c143a592562b77012dd739c02364bd1a9d6bbd6c7ede4f3a5a38b7f4a gencode/java/udmi/schema/DiscoveryModel.java 0fd348086bfd8e38a54a06b0fd205f6d01fdc6236f07ccc8bef49a1832f8c767 gencode/java/udmi/schema/DiscoveryState.java baef666f5f58f790b73ba7bed8f60eacdd75ba3151b9abc32265954a9da84faa gencode/java/udmi/schema/Electricity.java @@ -77,7 +77,7 @@ f890a14567657f5c97c2d3c1c753d751025ee094700ef0659f63f448529cc529 gencode/java/u 9df1e0a689390bda0847a0acab5a13fd7ced8c09b8563c13a7ce000ef1ea4999 gencode/java/udmi/schema/Events.java 8aed152c906c51a72a9224e7aec14bcdbd9ac2e00c2b1fed6bd5515082f79488 gencode/java/udmi/schema/ExecutionConfiguration.java a5a41a2bc809988efbbc121cdf730cbc1eb066372e046747fb6ae3f30e96a8b1 gencode/java/udmi/schema/FamilyDiscovery.java -0de804fb208fdf2ef198539ed5b828b780bc6c5cdd9e31b10067372165c4b7d4 gencode/java/udmi/schema/FamilyDiscoveryConfig.java +60286ff934d1ad721d90b2691538700073b73376207b06badfbd3f65e9d51b29 gencode/java/udmi/schema/FamilyDiscoveryConfig.java 6f9eaf4b56d3666caa65be4ded3344cb5b59aa9f838b454b5a4983d68061163e gencode/java/udmi/schema/FamilyDiscoveryModel.java 1d1f5a6ebf1db5f06976c6e4fb14f1404eec5460b9eb34f0508e1f95aa69e81b gencode/java/udmi/schema/FamilyDiscoveryState.java 180a65a6c61b76cbec7bb45bbfdfe0d4f26170d7f7e74aef9bd129f1dd451edf gencode/java/udmi/schema/FamilyLocalnetConfig.java @@ -176,7 +176,7 @@ e92d040fe84cc1bbc6e6b2d81a39dae156e1501e1c07a0b6cfc42e1599403e64 gencode/python 0feda2aadb7b5cbfbadc1ea6950292f428b765f8f27e874414d3ca9941f3746c gencode/python/udmi/schema/config_blobset.py 234b237a299d31b92dd873af6713c56e388345faeb79cbf2b46090c7d10ef00a gencode/python/udmi/schema/config_blobset_blob.py 246f6878e5a4b7f5e1a27d1c51aadd42bef07ee2113b9cd568bb623b24bc85de gencode/python/udmi/schema/config_discovery.py -5815f7f1ec7e5a2d5b7e3065060872d109ebb64f595c004a4b5b490c918684c2 gencode/python/udmi/schema/config_discovery_family.py +5445564598da1d534158603b7a0e42f1528629b8fdfcba3b4807e84510ff79e9 gencode/python/udmi/schema/config_discovery_family.py 326edbdbb90e376375294ba645fd789d1ae1809e8a98f3b32c3d04c2bc316ac1 gencode/python/udmi/schema/config_discovery_trace.py 0d100d1fec28c4e59cba704e5a512549972f427ea4da268c7472208fdccd96a6 gencode/python/udmi/schema/config_gateway.py 8119455e323f686da48ccf65d494d0dc2a26ad2a2a02d0f0167dc9a020740225 gencode/python/udmi/schema/config_localnet.py @@ -203,7 +203,7 @@ ab1a8ee1f12d04dd88837b6f026030b5ed3550ef3bfbbfbcb4f77cc4df39e93a gencode/python 3dcf4daa94b8450b7360e58500e20e1fbe5701502ac28358112ee1a6bd2a87f1 gencode/python/udmi/schema/events.py af48a2d32b4ec326df6e3f165d8944b994fb426bd4e663c42c1d592f5cb41f83 gencode/python/udmi/schema/events_alarmset.py b58371ffa7979d1598502a35859f3010ae7a2ec2d851454c010c835e426c7a14 gencode/python/udmi/schema/events_alarmset_alarm.py -3996175098735e889730c87ffc9ce1648e6624d801694458f6d4bc147ff29741 gencode/python/udmi/schema/events_discovery.py +ad1e97abeee523cdfe5c1c560f44da356eb30ef4cb1c464ef75e32bfbecbb6fc gencode/python/udmi/schema/events_discovery.py 84700f9d8a8975130ff9c62891003f7a12f367fa7dd1ca1db5f5fae7225bf005 gencode/python/udmi/schema/events_pointset.py 9f568bd573d4992e3efa3cc3cb8cdefecb95b15675b0442873abdc7850d108f9 gencode/python/udmi/schema/events_pointset_point.py 282439e4983ae9f310fda69dd6ff979567fa983ebdefcde407df42d631fe1bad gencode/python/udmi/schema/events_streams.py @@ -212,7 +212,7 @@ e4cdab07942e52cae243b4dddbc72676794914947dd66df8add0f5969f31fd11 gencode/python 72559779680c79dace3c5903022709852cbad7b77ed42af97debc8d852d61cb7 gencode/python/udmi/schema/events_validation.py a3ea178f98a4993da708e2ec41f5ede74aa92d742ffbe201a77adef803d4143a gencode/python/udmi/schema/events_validation_device.py 5660433b409bdaeacdce6b128660914586cca434e5373b3f94c04e89952642b4 gencode/python/udmi/schema/level.py -9577d11c5110af169aafd0e083d115c849ce17dfdc2b6b23b7186c69c1c333db gencode/python/udmi/schema/metadata.py +7595bc97beb84246bed974ec210c3c80f82926342c3cd062aa71d6805999eb35 gencode/python/udmi/schema/metadata.py cd81df64d7db21ab06f84cdbd07a808a641612c7c1124f9f2d928a898c3c767e gencode/python/udmi/schema/model_alarmset.py b80b7f580fd7527da0c2caff063218488cf5bbd091daa64a6a5f9a5d45b135c2 gencode/python/udmi/schema/model_alarmset_alarm.py 6d19eb94063acbc8e5432b9827e8b20f4f8cd02efa6ae760fabe60652c69542d gencode/python/udmi/schema/model_cloud.py diff --git a/bin/gencode_py b/bin/gencode_py index 6de1a7bd84..901100df08 100755 --- a/bin/gencode_py +++ b/bin/gencode_py @@ -69,6 +69,50 @@ bin/gencode_py_categories # create bucket.py from docs/specs/buckets.md bin/gencode_py_buckets +# Resolve mutual circular imports by moving them to the bottom of the module (after class definitions) +python3 - "$PYTHON_OUTPUT_DIR" << 'EOF' +import os, re, sys + +out_dir = sys.argv[1] +import_re = re.compile(r"^from \.([a-zA-Z0-9_]+) import .+$") +deps = {} +lines_by_mod = {} + +for fname in sorted(os.listdir(out_dir)): + if not fname.endswith(".py") or fname == "__init__.py": + continue + mod = fname[:-3] + path = os.path.join(out_dir, fname) + with open(path, "r", encoding="utf-8") as f: + lines = f.readlines() + lines_by_mod[mod] = (path, lines) + deps[mod] = set() + for line in lines: + m = import_re.match(line.strip()) + if m: + deps[mod].add(m.group(1)) + +for mod, (path, lines) in lines_by_mod.items(): + cyclic_targets = {target for target in deps[mod] if mod in deps.get(target, set())} + if not cyclic_targets: + continue + kept = [] + deferred = [] + for line in lines: + m = import_re.match(line.strip()) + if m and m.group(1) in cyclic_targets: + deferred.append(line.rstrip("\n") + "\n") + else: + kept.append(line) + if deferred: + if kept and not kept[-1].endswith("\n"): + kept[-1] += "\n" + kept.append("\n") + kept.extend(deferred) + with open(path, "w", encoding="utf-8") as f: + f.writelines(kept) +EOF + # add import statements to udmi/schema/__init__.py find "$PYTHON_OUTPUT_DIR" -name "*.py" -not -name "__init__.py" -print0 | while IFS= read -r -d $'\0' file; do module_name=$(basename "$file" .py) diff --git a/bin/gencode_root_schemas b/bin/gencode_root_schemas index b3f227680e..e785aa3cc0 100755 --- a/bin/gencode_root_schemas +++ b/bin/gencode_root_schemas @@ -21,7 +21,8 @@ IGNORE_LIST = ['events.json'] # These are sometimes referenced (e.g. by pubber) but should be considered root ALWAYS_ROOT = ['configuration_endpoint.json', 'events_system.json', - 'events_discovery.json'] + 'events_discovery.json', + 'metadata.json'] def parse_command_line_args(): parser = argparse.ArgumentParser() diff --git a/bin/gendocs.py b/bin/gendocs.py index dea71e8a90..ff5f0a5d5c 100644 --- a/bin/gendocs.py +++ b/bin/gendocs.py @@ -4,6 +4,34 @@ from json_schema_for_humans.generate import generate_from_filename from json_schema_for_humans.generation_configuration import GenerationConfiguration +import json_schema_for_humans.schema.intermediate_representation as ir +from json_schema_for_humans.schema.schema_node import SchemaNode + +# pylint: disable=protected-access,invalid-name +orig_has_ref_to_parent = ir._has_reference_to_parent +orig_node_is_parent = SchemaNode.node_is_parent + + +def _has_reference_to_parent(referenced_schema_path, anchor_part, current_node): + node = current_node + while node: + if orig_has_ref_to_parent(referenced_schema_path, anchor_part, node): + return True + node = node.parent + return False + + +def _node_is_parent(self, node_to_check): + node = self + while node: + if orig_node_is_parent(node, node_to_check): + return True + node = node.parent + return False + + +ir._has_reference_to_parent = _has_reference_to_parent +SchemaNode.node_is_parent = _node_is_parent config = GenerationConfiguration(copy_css=True, expand_buttons=True, diff --git a/bin/test_butler b/bin/test_butler index 0a58c98b54..4342e8d1ab 100755 --- a/bin/test_butler +++ b/bin/test_butler @@ -24,14 +24,14 @@ PG_DB=${POSTGRES_DB:-postgres} INF_PORT=${INFLUX_PORT:-8086} # Check if PostgreSQL and InfluxDB are running -if psql -h 127.0.0.1 -p "$PG_PORT" -U "$PG_USER" -d "$PG_DB" -c "SELECT 1;" >/dev/null 2>&1; then +if psql -w -h 127.0.0.1 -p "$PG_PORT" -U "$PG_USER" -d "$PG_DB" -c "SELECT 1;" >/dev/null 2>&1; then echo "PostgreSQL is active on port $PG_PORT." echo "Piping simple trace into Butler..." bin/pull_files tests/traces/simple | butler/bin/butler --stdin --always-save-raw echo "Verifying PostgreSQL records in udmi_messages..." - msg_count=$(psql -h 127.0.0.1 -p "$PG_PORT" -U "$PG_USER" -d "$PG_DB" -t -A -c "SELECT count(*) FROM udmi_messages;" 2>/dev/null || echo 0) + msg_count=$(psql -w -h 127.0.0.1 -p "$PG_PORT" -U "$PG_USER" -d "$PG_DB" -t -A -c "SELECT count(*) FROM udmi_messages;" 2>/dev/null || echo 0) echo "udmi_messages count: $msg_count" if [[ $msg_count -gt 0 ]]; then @@ -42,10 +42,10 @@ if psql -h 127.0.0.1 -p "$PG_PORT" -U "$PG_USER" -d "$PG_DB" -c "SELECT 1;" >/de fi echo "Verifying PostgreSQL records in specialized tables..." - sys_count=$(psql -h 127.0.0.1 -p "$PG_PORT" -U "$PG_USER" -d "$PG_DB" -t -A -c "SELECT count(*) FROM udmi_system_state;" 2>/dev/null || echo 0) + sys_count=$(psql -w -h 127.0.0.1 -p "$PG_PORT" -U "$PG_USER" -d "$PG_DB" -t -A -c "SELECT count(*) FROM udmi_system_state;" 2>/dev/null || echo 0) echo "udmi_system_state count: $sys_count" - pt_count=$(psql -h 127.0.0.1 -p "$PG_PORT" -U "$PG_USER" -d "$PG_DB" -t -A -c "SELECT count(*) FROM udmi_point_state;" 2>/dev/null || echo 0) + pt_count=$(psql -w -h 127.0.0.1 -p "$PG_PORT" -U "$PG_USER" -d "$PG_DB" -t -A -c "SELECT count(*) FROM udmi_point_state;" 2>/dev/null || echo 0) echo "udmi_point_state count: $pt_count" else echo "PostgreSQL is not running locally; skipping live database verification (tested via unit tests with mocks)." diff --git a/bin/test_uufi_client b/bin/test_uufi_client index 773eea59f3..7c41824823 100755 --- a/bin/test_uufi_client +++ b/bin/test_uufi_client @@ -196,5 +196,55 @@ restore_metadata trap - EXIT echo "System Model Update SUCCESS: Received matching model update message on bus" +# 4. Site Model Discovery Test +echo "Starting Site Model Discovery test..." +DISC_LOG_FILE=$(mktemp) +DISC_GEN=$(date -u +%Y-%m-%dT%H:%M:%SZ) +echo "Subscribing to /uufi/r/$REGISTRY_ID/# for Model Discovery sequence (gen: $DISC_GEN)..." +mosquitto_sub $MQTT_ARGS -i "${CLIENT_ID}_disc_sub" -t "/uufi/r/$REGISTRY_ID/#" -F "%t %p" | while read -r topic payload; do + if [[ "$topic" == *"/discovery" ]]; then + echo "$topic $payload" >> "$DISC_LOG_FILE" + SUB_TYPE=$(echo "$payload" | jq -r .subType 2>/dev/null) + PHASE=$(echo "$payload" | jq -r .payload.families.iot.phase 2>/dev/null) + GEN=$(echo "$payload" | jq -r .payload.families.iot.generation 2>/dev/null) + if [[ "$SUB_TYPE" == "state" && "$PHASE" == "stopped" && "$GEN" == "$DISC_GEN" ]]; then + kill -USR1 $$ 2>/dev/null || true + exit 0 + fi + fi +done & +SUB_DISC_PID=$! + +GOT_DISC=0 +trap "GOT_DISC=1" USR1 +sleep 2 + +DISC_CONFIG="{\"subType\":\"config\",\"subFolder\":\"discovery\",\"deviceRegistryId\":\"$REGISTRY_ID\",\"payload\":{\"version\":\"1.5.7\",\"timestamp\":\"$DISC_GEN\",\"generation\":\"$DISC_GEN\",\"families\":{\"iot\":{\"generation\":\"$DISC_GEN\",\"depth\":\"details\",\"source\":\"model\"}}}}" +echo "Publishing Model Discovery config to /uufi/r/$REGISTRY_ID/c/config/discovery: $DISC_CONFIG" +mosquitto_pub $MQTT_ARGS -i "${CLIENT_ID}_disc_pub" -t "/uufi/r/$REGISTRY_ID/c/config/discovery" -m "$DISC_CONFIG" + +TIMEOUT=30 +while [ $GOT_DISC -eq 0 ] && [ $TIMEOUT -gt 0 ]; do + echo "Waiting for Model Discovery completion... ($TIMEOUT)" + sleep 1 + TIMEOUT=$((TIMEOUT - 1)) +done + +kill $SUB_DISC_PID 2>/dev/null || true + +if [ $GOT_DISC -eq 0 ]; then + echo "ERROR: Timed out waiting for Model Discovery sequence completion" >&2 + cat "$DISC_LOG_FILE" || true + exit 1 +fi + +# Verify active state, event_no=0 count, AHU-1 model event, and stopped state +grep -q '"phase":"active"' "$DISC_LOG_FILE" || { echo "ERROR: Missing active DiscoveryState in Model Discovery" >&2; cat "$DISC_LOG_FILE"; exit 1; } +grep -q '"event_no":0' "$DISC_LOG_FILE" || { echo "ERROR: Missing event_no=0 DiscoveryEvents in Model Discovery" >&2; cat "$DISC_LOG_FILE"; exit 1; } +grep -q "/uufi/r/$REGISTRY_ID/d/$DUT_ID/c/events/discovery" "$DISC_LOG_FILE" || { echo "ERROR: Missing $DUT_ID DiscoveryEvents in Model Discovery" >&2; cat "$DISC_LOG_FILE"; exit 1; } +grep -q '"source":"model"' "$DISC_LOG_FILE" || { echo "ERROR: Missing source:model in Model Discovery messages" >&2; cat "$DISC_LOG_FILE"; exit 1; } +grep -q '"phase":"stopped"' "$DISC_LOG_FILE" || { echo "ERROR: Missing stopped DiscoveryState in Model Discovery" >&2; cat "$DISC_LOG_FILE"; exit 1; } +echo "Site Model Discovery SUCCESS: Verified start state, event_no=0 count, device model events, and stop state" + echo "UUFI Comprehensive Integration Test PASSED" -rm -f "$REPLY_FILE" "$STATE_REPLY_FILE" "$MODEL_REPLY_FILE" +rm -f "$REPLY_FILE" "$STATE_REPLY_FILE" "$MODEL_REPLY_FILE" "$DISC_LOG_FILE" diff --git a/butler/src/connection.py b/butler/src/connection.py index 4bd33a898a..33741a777e 100644 --- a/butler/src/connection.py +++ b/butler/src/connection.py @@ -241,8 +241,13 @@ def _resolve_endpoint(self): self.username = f"/r/{ns_prefix}UDMI-REFLECT/d/{self.actual_registry}" self.password = derived_pass - def get_discovery_topics(self, device_id): - """Returns list of topics to publish discovery config for target device.""" + def get_discovery_topics(self, device_id=None): + """Returns list of topics to publish discovery config for target device or registry.""" + if not device_id: + uufi_topic = f"/uufi/r/{self.actual_registry}/c/config/discovery" + if self.prefix: + uufi_topic = f"/{self.prefix}{uufi_topic}" + return [uufi_topic] if self.provider in ("gbos", "clearblade"): return [ f"/devices/{device_id}/commands/discovery", diff --git a/butler/src/discovery.py b/butler/src/discovery.py index 71d98f2ae7..9392810565 100644 --- a/butler/src/discovery.py +++ b/butler/src/discovery.py @@ -10,16 +10,28 @@ from butler.src.connection import ButlerConnection -def run_discovery(conn_spec, registry_id, device_id, target_families, site_model=None): +def run_discovery(conn_spec, registry_id, device_id=None, target_families=None, site_model=None): """Dispatches discovery configuration message over the configured project spec / broker.""" - connection = ButlerConnection(conn_spec, registry_id, site_model, device_id) + is_registry_discovery = not device_id + actual_device_id = None if is_registry_discovery else device_id + connection = ButlerConnection(conn_spec, registry_id, site_model, actual_device_id) actual_registry = connection.actual_registry now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") families_dict = {} + if is_registry_discovery and not target_families: + target_families = ["iot"] + if target_families: for f in target_families: - families_dict[f] = {"generation": now} + if f == "iot": + families_dict[f] = { + "generation": now, + "depth": "details", + "source": "model", + } + else: + families_dict[f] = {"generation": now} else: families_dict = { "vendor": {"generation": now}, @@ -30,17 +42,21 @@ def run_discovery(conn_spec, registry_id, device_id, target_families, site_model tx_id = f"TXN-{uuid.uuid4().hex[:8]}" source_id = "butler" payload = { - "version": "1.5.2", + "version": "1.5.7", "timestamp": now, "generation": now, "families": families_dict, - "enumerations": {"families": "entries", "points": "entries", "features": "entries"}, } + if not is_registry_discovery: + payload["enumerations"] = { + "families": "entries", + "points": "entries", + "features": "entries", + } msg = { "subType": "config", "subFolder": "discovery", "deviceRegistryId": actual_registry, - "deviceId": device_id, "projectId": connection.project or "vibrant", "transactionId": tx_id, "publishTime": now, @@ -48,7 +64,9 @@ def run_discovery(conn_spec, registry_id, device_id, target_families, site_model "principal": source_id, "payload": payload, } + if actual_device_id: + msg["deviceId"] = actual_device_id - topics = connection.get_discovery_topics(device_id) + topics = connection.get_discovery_topics(actual_device_id) topic_message_pairs = [(t, msg) for t in topics] connection.publish_messages(topic_message_pairs) diff --git a/butler/src/handlers/discovery.py b/butler/src/handlers/discovery.py index 5370102172..6eda9cbdba 100644 --- a/butler/src/handlers/discovery.py +++ b/butler/src/handlers/discovery.py @@ -2,20 +2,30 @@ from typing import Any, Dict, List, Optional from butler.src.handlers.base import BaseHandler +from butler.src.handlers.metadata import MetadataHandler from udmi.common.db.influx import InfluxManager from udmi.common.db.postgres import PostgresManager def get_discovered_family_addr(payload: Dict[str, Any], family: str) -> Optional[str]: """Returns the discovered address for a given family.""" - try: - if family == payload.get("family"): - return str(payload.get("addr")) - families = payload.get("families", {}) - if isinstance(families, dict) and family in families: - return str(families[family].get("addr")) - except Exception: - pass + if not isinstance(payload, dict): + return None + if family == payload.get("family") and payload.get("addr") is not None: + return str(payload.get("addr")) + families = payload.get("families") + if isinstance(families, dict): + family_block = families.get(family) + if isinstance(family_block, dict) and family_block.get("addr") is not None: + return str(family_block.get("addr")) + model = payload.get("model") + if isinstance(model, dict): + localnet = model.get("localnet") + model_families = localnet.get("families") if isinstance(localnet, dict) else None + if isinstance(model_families, dict): + family_block = model_families.get(family) + if isinstance(family_block, dict) and family_block.get("addr") is not None: + return str(family_block.get("addr")) return None @@ -23,7 +33,7 @@ class DiscoveryHandler(BaseHandler): """Processes discovery events and saves rows into PostgreSQL udmi_discovery table.""" def can_handle(self, sub_folder: Optional[str], sub_type: Optional[str], payload: Dict[str, Any]) -> bool: - return sub_folder == "discovery" + return sub_folder == "discovery" and sub_type in (None, "events") def process_message( self, @@ -35,6 +45,16 @@ def process_message( if isinstance(payload, dict) and "payload" in payload and isinstance(payload.get("payload"), dict): payload = payload["payload"] family = payload.get("family") + event_no = payload.get("event_no") + model_data = payload.get("model") if isinstance(payload.get("model"), dict) else {} + + if isinstance(event_no, int) and event_no <= 0 and not payload.get("addr") and not model_data: + return { + "target": "postgres", + "table": "udmi_discovery", + "count": 0, + } + ports: List[Dict[str, Any]] = [] if family == "ether" and (refs := payload.get("refs")): if isinstance(refs, dict): @@ -45,16 +65,21 @@ def process_message( if isinstance(v, dict) ] - system = payload.get("system", {}) if isinstance(payload.get("system"), dict) else {} + raw_system = payload.get("system") if isinstance(payload.get("system"), dict) else model_data.get("system", {}) + system = raw_system if isinstance(raw_system, dict) else {} hardware = system.get("hardware", {}) if isinstance(system.get("hardware"), dict) else {} ancillary = system.get("ancillary", {}) if isinstance(system.get("ancillary"), dict) else {} status = payload.get("status", {}) if isinstance(payload.get("status"), dict) else {} + device_id = envelope.get("deviceId") + if not device_id and family == "iot" and payload.get("addr"): + device_id = str(payload.get("addr")) + row = { "timestamp": payload.get("timestamp") or envelope.get("publishTime"), "generation": payload.get("generation"), "device_registry_id": envelope.get("deviceRegistryId"), - "device_id": envelope.get("deviceId"), + "device_id": device_id, "message_id": envelope.get("messageId"), "scan_family": family, "ether_addr": get_discovered_family_addr(payload, "ether"), @@ -74,6 +99,12 @@ def process_message( postgres_manager.insert_row("udmi_discovery", row) + if family == "iot" and model_data: + model_env = dict(envelope) + if device_id: + model_env["deviceId"] = device_id + MetadataHandler().process_message(model_env, model_data, postgres_manager, influx_manager) + return { "target": "postgres", "table": "udmi_discovery", diff --git a/butler/src/mapping.py b/butler/src/mapping.py index 8f8777589e..b02cbc4d28 100644 --- a/butler/src/mapping.py +++ b/butler/src/mapping.py @@ -58,7 +58,7 @@ def run_mapping( butler = butler_client or get_butler_interface(conn_spec, butler_port) # 1. Get most recent model for each device_id in this registry - models = [] + models_by_id = {} if site_model: devices_dir = os.path.join(site_model, "devices") if os.path.exists(devices_dir): @@ -67,9 +67,19 @@ def run_mapping( if os.path.exists(meta_path): try: with open(meta_path, "r", encoding="utf-8") as mf: - models.append((device_id, json.load(mf))) + models_by_id[device_id] = json.load(mf) except Exception as e: print(f"err: {e}", file=sys.stderr) + if butler and hasattr(butler, "get_discovered_models"): + try: + discovered_models = butler.get_discovered_models(registry_id) + if isinstance(discovered_models, dict): + for dev_id, model_payload in discovered_models.items(): + if isinstance(model_payload, dict) and dev_id not in models_by_id: + models_by_id[dev_id] = model_payload + except Exception as e: + print(f"Error querying discovered models via Butler: {e}", file=sys.stderr) + models = list(models_by_id.items()) print(f"Found {len(models)} models", file=sys.stderr) # 2. Get discovered devices via Butler interface diff --git a/butler/tests/test_handlers.py b/butler/tests/test_handlers.py index 18df34746b..f135b8a291 100644 --- a/butler/tests/test_handlers.py +++ b/butler/tests/test_handlers.py @@ -181,6 +181,115 @@ def test_raw_fallback_handler(self): self.assertEqual(row["project_id"], "proj-1") self.assertEqual(row["payload"], payload) + def test_model_discovery_handler(self): + handler = DiscoveryHandler() + self.assertTrue(handler.can_handle("discovery", "events", {})) + self.assertFalse(handler.can_handle("discovery", "state", {})) + + start_env = {"deviceRegistryId": "reg-1"} + start_payload = { + "family": "iot", + "generation": "2026-08-21T00:00:00Z", + "event_no": 0, + "active_count": 1, + } + start_res = handler.process_message(start_env, start_payload, self.mock_pg, self.mock_influx) + self.assertEqual(start_res["count"], 0) + self.mock_pg.insert_row.assert_not_called() + + envelope = {"deviceId": "AHU-1", "deviceRegistryId": "reg-1"} + payload = { + "family": "iot", + "generation": "2026-08-21T00:00:00Z", + "event_no": 1, + "addr": "AHU-1", + "model": { + "version": "1.5.7", + "timestamp": "2026-08-21T00:00:00Z", + "system": { + "hardware": {"make": "Acme", "model": "AHU-Pro"}, + "serial_no": "SN-AHU-1", + "location": {"site": "reg-1", "section": "Floor-2", "position": {"x": 1, "y": 2}}, + }, + "localnet": { + "families": { + "bacnet": {"addr": "41122"}, + "ipv4": {"addr": "10.0.0.10"}, + } + }, + }, + } + + res = handler.process_message(envelope, payload, self.mock_pg, self.mock_influx) + self.assertEqual(res["target"], "postgres") + self.assertEqual(res["table"], "udmi_discovery") + self.assertEqual(self.mock_pg.insert_row.call_count, 2) + disc_table, disc_row = self.mock_pg.insert_row.call_args_list[0][0] + meta_table, meta_row = self.mock_pg.insert_row.call_args_list[1][0] + self.assertEqual(disc_table, "udmi_discovery") + self.assertEqual(disc_row["scan_family"], "iot") + self.assertEqual(disc_row["bacnet_addr"], "41122") + self.assertEqual(disc_row["ipv4_addr"], "10.0.0.10") + self.assertEqual(disc_row["hardware_make"], "Acme") + self.assertEqual(meta_table, "udmi_metadata") + self.assertEqual(meta_row["device_id"], "AHU-1") + self.assertEqual(meta_row["metadata"]["system"]["serial_no"], "SN-AHU-1") + + def test_registry_model_discovery_topics(self): + from butler.src.connection import ButlerConnection + conn = ButlerConnection("//mqtt/localhost:18833", registry_id="ZZ-TRI-FECTA") + self.assertEqual( + conn.get_discovery_topics(None), + ["/uufi/r/ZZ-TRI-FECTA/c/config/discovery"], + ) + self.assertEqual( + conn.get_discovery_topics("GAT-123"), + ["/uufi/r/ZZ-TRI-FECTA/d/GAT-123/c/config/discovery"], + ) + + def test_mapping_with_discovered_models(self): + from unittest.mock import patch + from butler.src.mapping import run_mapping + + mock_butler = MagicMock() + mock_butler.get_discovered_models.return_value = { + "AHU-1": { + "version": "1.5.2", + "timestamp": "2026-08-21T00:00:00Z", + "system": {"serial_no": "SN-AHU-1"}, + "localnet": { + "families": { + "bacnet": {"addr": "41122"}, + } + }, + } + } + mock_butler.get_discovered_devices.return_value = [ + { + "gateway_id": "GAT-123", + "generation": "2026-08-21T00:00:00Z", + "bacnet": "41122", + "ipv4": "192.168.1.50", + "vendor": None, + "timestamp": "2026-08-21T00:00:01Z", + } + ] + + with patch("butler.src.mapping.ButlerConnection") as mock_conn_cls: + mock_conn = MagicMock() + mock_conn.actual_registry = "ZZ-TRI-FECTA" + mock_conn.project = "localhost" + mock_conn.get_propose_topics.side_effect = ( + lambda dev_id, sf="system": [f"/uufi/r/ZZ-TRI-FECTA/d/{dev_id}/c/propose/{sf}"] + ) + mock_conn_cls.return_value = mock_conn + + run_mapping("//mqtt/localhost:18833", "ZZ-TRI-FECTA", site_model=None, butler_client=mock_butler) + mock_conn.publish_messages.assert_called_once() + published_pairs = mock_conn.publish_messages.call_args[0][0] + topics = [t for t, _ in published_pairs] + self.assertIn("/uufi/r/ZZ-TRI-FECTA/d/AHU-1/c/propose/localnet", topics) + if __name__ == "__main__": unittest.main() diff --git a/docs/specs/discovery.md b/docs/specs/discovery.md index fd2f9c28ea..9a2e6cedcc 100644 --- a/docs/specs/discovery.md +++ b/docs/specs/discovery.md @@ -79,3 +79,14 @@ be reported: * `blobs`: A listing of all the data blobs that a device knows how to handle. This could be components like firmware updates, key rotation, etc... Some blobs will be standardized across the system, while others will be device-specific. + +## Site Model Discovery + +Site Model Discovery exposes the cloud-registered device models of a registry over the standard UDMI discovery pipeline (`family: "iot"`, `source: "model"`). It is triggered automatically when a registry model update is processed by UDMIS, or on-demand by publishing a registry-scoped [`config`](../../gencode/docs/config.html) discovery message (e.g., on `/uufi/r/{deviceRegistryId}/c/config/discovery` with `families.iot.source` set to `"model"`). + +The Site Model Discovery sequence follows the standard discovery state and event bookends: + +1. **Start State (`state/discovery`):** Published at the registry level with `source: "model"`, `families.iot.phase: "active"`, and `families.iot.active_count: 0`. +2. **Start & Count Event (`events/discovery`):** Published at the registry level with `source: "model"`, `event_no: 0`, `family: "iot"`, and `active_count` set to the total number of matching devices in the registry. +3. **Device Model Events (`events/discovery`):** When `depth` is `details` (or `parts`, or omitted), one event per device (`event_no: 1..N`) is published with `source: "model"`, `family: "iot"`, `addr` set to the device ID, and `model` populated with the device's [`metadata`](../../gencode/docs/metadata.html) object. When `depth` is `entries`, individual device events are omitted and only the `event_no: 0` count is emitted. If `addrs` is specified in `families.iot`, only matching device IDs are included. +4. **Stop State (`state/discovery`):** Published at the registry level with `source: "model"`, `families.iot.phase: "stopped"`, and `families.iot.active_count` set to the total number of discovered devices. diff --git a/docs/specs/uufi.md b/docs/specs/uufi.md index 0617f6ddd9..1124ba74bb 100644 --- a/docs/specs/uufi.md +++ b/docs/specs/uufi.md @@ -50,7 +50,7 @@ Format: `scheme://[user@]host[:port][/path]` - The connection MUST utilize SSL/TLS with mutual certificate-based authentication (mTLS) where applicable. - Implementations MUST treat `mqtts://` with the same routing, parsing, and topic mapping rules as `mqtt://`, differing only in the enforcement of SSL/TLS. - **Topic Structure:** `[/{prefix}]/uufi/[r/{deviceRegistryId}/[d/{deviceId}/]]c/{subType}/{subFolder}` - - **Rigid Segment Presence:** Every UUFI topic path MUST include both a subtype (`subType`) and a subfolder (`subFolder`) segment without exception. Topic paths MUST be formatted strictly as `[/{prefix}]/uufi/c/{subType}/{subFolder}` (for common channels) or `[/{prefix}]/uufi/r/{deviceRegistryId}/d/{deviceId}/c/{subType}/{subFolder}` (for registry/device scoped channels). Omitting the subfolder segment or truncating suffixes (e.g. `/c/{subType}`) is strictly prohibited. + - **Rigid Segment Presence:** Every UUFI topic path MUST include both a subtype (`subType`) and a subfolder (`subFolder`) segment without exception. Topic paths MUST be formatted strictly as `[/{prefix}]/uufi/c/{subType}/{subFolder}` (for common channels), `[/{prefix}]/uufi/r/{deviceRegistryId}/c/{subType}/{subFolder}` (for registry-scoped channels such as Site Model Discovery), or `[/{prefix}]/uufi/r/{deviceRegistryId}/d/{deviceId}/c/{subType}/{subFolder}` (for device-scoped channels). Omitting the subfolder segment or truncating suffixes (e.g. `/c/{subType}`) is strictly prohibited. - **Handshake Subfolder Suffix:** For standard registry-less handshakes, the subfolder segment MUST be explicitly set to `"udmi"`. Thus, the handshake topics MUST be exactly `[/{prefix}]/uufi/c/state/udmi` and `[/{prefix}]/uufi/c/config/udmi`. - **Prohibition of Alternative Topic Structures:** Principal-scoped patterns (such as `[/{prefix}]/uufi/p/{principal}/...`) and any other custom or arbitrary routing hierarchies are strictly prohibited. All system components MUST standardize exclusively on the common and registry-scoped topic channels to ensure interoperability. - **Prefix Isolation:** The `prefix` MUST be used to isolate different environments sharing the same broker. If provided, it MUST be the leading part of the topic path (e.g. matching all segments of the path provided in the connection string). Implementations MUST support multi-segment prefixes and MUST NOT omit the prefix if provided in the connection string. All active subscriptions MUST be scoped to the provided prefix to ensure environmental isolation. Prefix enforcement MUST be strict: implementations MUST NOT publish to or subscribe from topics outside their designated prefix tree. To avoid collisions and support standard broker authorization structures, implementations MUST construct unique MQTT Client IDs adhering to a standardized format: diff --git a/gencode/docs/config.html b/gencode/docs/config.html index 8378828bfd..9660a5d2d5 100644 --- a/gencode/docs/config.html +++ b/gencode/docs/config.html @@ -2148,6 +2148,61 @@

Must be one of:

+ + + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Optional source qualifier for the discovery scan (e.g., 'model')

+
+ + + + + +
diff --git a/gencode/docs/events.html b/gencode/docs/events.html index 618c455d31..c784050761 100644 --- a/gencode/docs/events.html +++ b/gencode/docs/events.html @@ -6746,6 +6746,47 @@

+

+ + + +
+
+
+

+ +

+
+ +
+
+ + Type: integer
+

Total number of records expected for this active scan generation

+
+ + + +

Value must be greater or equal to 0

+ +
@@ -25538,6 +25579,27005 @@

"1"
 
+
+ + + + + + + + + + + + +
+
+
+

+ +

+
+ +
+
+ +

Metadata

Type: object
+

Device metadata model (for model discovery)

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: stringFormat: date-time
+

RFC 3339 timestamp UTC the data was generated

+
+ + + + + +
+
Example:
+
"2019-01-17T14:02:29.364Z"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Version of the UDMI schema for this file

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Original version of the UDMI schema for this file

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

DEPRECATED

+
Must match regular expression: ^[0-9a-z]{8}$ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ +

Model Operation

Type: enum (of string)
+

Used for model update operations (so not a model proper), e.g. DELETE or BIND operations

+
+ +
+

Must be one of:

+
  • "ERROR"
  • "CREATE"
  • "READ"
  • "REPLY"
  • "UPDATE"
  • "DELETE"
  • "MODIFY"
  • "PREVIEW"
  • "BIND"
  • "UNBIND"
  • "BOUND"
  • "ALLOW"
  • "BLOCK"
+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ +

Cloud Model

Type: object
+

Information specific to how the device communicates with the cloud.

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: integer
+

Function version of this request

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: enum (of string)
+

The key type used for cloud communication.

+
+

Must be one of:

+
  • "ES256"
  • "ES256_X509"
  • "RS256"
  • "RS256_X509"
+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: boolean
+

Indicates if the device private key is provided in the site model.

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: enum (of string)
+

The cloud resource type

+
+

Must be one of:

+
  • "DIRECT"
  • "GATEWAY"
  • "FUNNEL"
  • "PROXIED"
  • "REGISTRY"
+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ +

Gateway Model

Type: object
+

Gateway Documentation

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+

The device ID of the gateway the device is bound to

+
Must match regular expression: ^[A-Z]{2,6}-[1-9][0-9]*$ + + + + + +
+
Example:
+
"GAT-100"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ +

Family Localnet Model

Type: object
+

The type of network

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+

The address of a device on the fieldbus/local network

+
+ + + + + +
+
Examples:
+
"192.168.1.3"
+
+
"41489"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

The slave unit ID of the device for protocol families like modbus

+
+ + + + + +
+
Examples:
+
"1"
+
+
"255"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

The network designator for this device in the family-defined format

+
+ + + + + +
+
Examples:
+
"192.168.1.0/24"
+
+
"12732"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+ No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+

+ +

+

All properties whose name matches the following regular expression must respect the following conditions

+ Property name regular expression: ^[a-z_]+$ +
+ + Type: string
+ + + + + + + +
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Specifies that this is a shadow of the indicated device

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

The device id of the node's parent

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: boolean
+

If true, then the system should not try to validate the refs

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

The family designator, used only when the entry is not keyed in a family map

+
+ + + + + +
+
Examples:
+
"ipv4"
+
+
"bacnet"
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: array of string
+

An array of all the device IDs which are bound to the device

+
+ + + + + + No Additional Items

Each item of this array must be:

+
+
+ + + Type: string
+Must match regular expression: ^[A-Z]{2,6}-[1-9][0-9]*$ + + + + + + +
+

+
Example:
+
[
+    "AHU-22"
+]
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ +

Config Cloud Model

Type: object
+

Information specific to how the device communicates with the cloud.

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Config file to use. Within the config directory in the device's metadata directory

+
+ + + + + + +
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: boolean
+ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: stringFormat: date-time
+ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: stringFormat: date-time
+ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: stringFormat: date-time
+ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: stringFormat: date-time
+ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ +

Model Operation

Type: enum (of string)
+ + +
+

Must be one of:

+
  • "ERROR"
  • "CREATE"
  • "READ"
  • "REPLY"
  • "UPDATE"
  • "DELETE"
  • "MODIFY"
  • "PREVIEW"
  • "BIND"
  • "UNBIND"
  • "BOUND"
  • "ALLOW"
  • "BLOCK"
+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Strigified version of the metadata object, used for internal backend processing

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+

If operating on the entire registry, then this contains results for multiple devices (batched operation).

+
+ + + + + + +
+
+
+

+ +

+
+ +
+

+ +

+

All properties whose name matches the following regular expression must respect the following conditions

+ Property name regular expression: ^[-._a-zA-Z0-9]+$ +
+ + Type: object
+ + + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ +

System Model

Type: object
+

High-level system information about the device. System Model Documentation

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Friendly name describing this device

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Generic human-readable text describing the device

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Version of schema supported by the device

+
+ + + + + +
+
Example:
+
"1.5.0"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Aggregation of spaces, partial spaces or other zones that the device is serving

+
Must match regular expression: ^[-_a-zA-Z0-9]+$ + + + + + +
+
Examples:
+
"Zone1"
+
+
"Kitchen_101"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

The description of what kind of node this is.

+
Must match regular expression: ^[a-z][_a-z0-9]+$ + + + + + +
+
Examples:
+
"vitual_device"
+
+
"field_group"
+
+
"field_controller"
+
+
"network_scan_agent"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: array
+

Tags associated with the device

+
+ + + + + +

All items must be unique

No Additional Items

Each item of this array must be:

+
+
+ + + Type: object
+Must match regular expression: ^[a-z][_a-z0-9]+$ + + + + + + +
+

+
Examples:
+
[
+    "lighting"
+]
+
+
[
+    "energy",
+    "building",
+    "vendor_managed"
+]
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+

Properties of the expected physical location of the device

+
No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+

The site name according to the site model in which the device is installed in

+
Must match regular expression: ^[A-Z]{2}-[A-Z]{3,4}-[A-Z0-9]{2,9}$ + + + + + +
+
Example:
+
"US-SFO-XYY"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

The reference of the panel where the device is installed in

+
Must match regular expression: ^[-_a-zA-Z0-9]+$ + + + + + +
+
Examples:
+
"Panel123"
+
+
"CTRP-567"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+Must match regular expression: ^[A-Z0-9-]+$ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+Must match regular expression: ^[-_a-zA-Z0-9]+$ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Name of floor level

+
Must match regular expression: ^[A-Z0-9-]+$ + + + + + +
+
Examples:
+
"1"
+
+
"1A"
+
+
"G"
+
+
"B1M"
+
+
"RF"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: integer
+

Sequential integer representation for a floor, primarily for comparisons when non integer floors are used, e.g. 1 and 1M

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+ No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: number
+

The x coordinate of the device location in a project specific coordinate system

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: number
+

The y coordinate of the device location in a project specific coordinate system

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: number
+

The z (height) coordinate of the device location in a project specific coordinate system

+
+ + + + + + +
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+ No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: number
+

The latitude coordinate of the device in the WGS84 coordinate system

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: number
+

The longitude coordinate of the device in the WGS84 coordinate system

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: number
+

The altitude coordinate of the device in meters

+
+ + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

The serial number of the physical device

+
+ + + + + +
+
Examples:
+
"A1B2C3D4"
+
+
"00001"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ +

SystemHardware

Type: object
+

A collection of fields which describe the physical hardware of the device.

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+

The manufacturer of the device

+
+ + + + + +
+
Example:
+
"Google"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

The model of the device

+
+ + + + + +
+
Examples:
+
"Pixel 5"
+
+
"Pixel 6"
+
+
"Pixel 6 Pro"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

A stock keeping unit which identifies the unique composition of a device, for example those with different hardware configurations

+
+ + + + + +
+
Examples:
+
"GF5KQ"
+
+
"4215612"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Hardware revision of the device

+
+ + + + + +
+
Examples:
+
"A"
+
+
"B"
+
+
"1"
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+

A collection of items which can be used to describe version of software running on a device

+
No Additional Properties + + + + + +
+
Examples:
+
"firmware"
+
+
"os"
+
+
"driver"
+
+
+
+
+
+

+ +

+
+ +
+

+ +

+

All properties whose name matches the following regular expression must respect the following conditions

+ Property name regular expression: ^[a-z_]+$ +
+ + Type: string
+ + + + + + + +
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+

Information used to print a physical QR code label.

+
No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: object
+ No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+Must match regular expression: ^[a-z]+://[-0-9a-zA-Z_$]+$ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+Must match regular expression: ^[A-Z]{2}-[A-Z]{3,4}-[A-Z0-9]{2,9}$ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+Must match regular expression: ^[A-Z]{2,6}-[1-9][0-9]*$ + + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+ No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+

+ +

+

All properties whose name matches the following regular expression must respect the following conditions

+ Property name regular expression: ^[a-z_]+$ +
+ + Type: string
+ + + + + + + +
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: integer Default: 300
+

The minimum loglevel for reporting log messages below which log entries should not be sent. Default to 300.

+
+ + + +

Value must be greater or equal to 100 and lesser or equal to 800

+ + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: integer
+

The rate at which the system should send system event updates. 0 indicates no updates.

+
+ + + +

Value must be greater or equal to 0 and lesser or equal to 86400

+ + +
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ +

Externals Model

Type: object
+

Information specific to how the device links with external models

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+

+ +

+

All properties whose name matches the following regular expression must respect the following conditions

+ Property name regular expression: ^[a-z]+(_[a-z]+)*$ +
+ +

Link Externals Model

Type: object
+

Information about how this device links to a specific external model

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Id of this device in the external model namespace

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Type of this device in the external model namespace

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Etag for referencing this external entity

+
Must match regular expression: ^[0-9a-f]+$ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Descriptive label for this entity

+
+ + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ +

Entity Relationships

Type: object
+

Entity relationships to other devices, keyed by target device ID

+
+ + + + + + + + +
+
+
+

+ +

+
+ +
+

+ +

+

All properties whose name matches the following regular expression must respect the following conditions

+ Property name regular expression: ^[-0-9a-zA-Z]+$ +
+ + Type: object
+ + + + + + + +
+
+
+

+ +

+
+ +
+

+ +

+

All properties whose name matches the following regular expression must respect the following conditions

+ Property name regular expression: ^[-0-9a-zA-Z_]+$ +
+ + Type: array of object
+ + + + + + + No Additional Items

Each item of this array must be:

+
+
+ + + Type: object
+ + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: array of string
+ + + + + + + No Additional Items

Each item of this array must be:

+
+
+ + + Type: string
+ + + + + + + +
+

+
Example:
+
[
+    "primary"
+]
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ +

Gateway Model

Type: object
+

Gateway Documentation

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+

The device ID of the gateway the device is bound to

+
Must match regular expression: ^[A-Z]{2,6}-[1-9][0-9]*$ + + + + + +
+
Example:
+
"GAT-100"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ +

Family Localnet Model

Type: object
+

The type of network

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+

The address of a device on the fieldbus/local network

+
+ + + + + +
+
Examples:
+
"192.168.1.3"
+
+
"41489"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

The slave unit ID of the device for protocol families like modbus

+
+ + + + + +
+
Examples:
+
"1"
+
+
"255"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

The network designator for this device in the family-defined format

+
+ + + + + +
+
Examples:
+
"192.168.1.0/24"
+
+
"12732"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+ No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+

+ +

+

All properties whose name matches the following regular expression must respect the following conditions

+ Property name regular expression: ^[a-z_]+$ +
+ + Type: string
+ + + + + + + +
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Specifies that this is a shadow of the indicated device

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

The device id of the node's parent

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: boolean
+

If true, then the system should not try to validate the refs

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

The family designator, used only when the entry is not keyed in a family map

+
+ + + + + +
+
Examples:
+
"ipv4"
+
+
"bacnet"
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: array of string
+

An array of all the device IDs which are bound to the device

+
+ + + + + + No Additional Items

Each item of this array must be:

+
+
+ + + Type: string
+Must match regular expression: ^[A-Z]{2,6}-[1-9][0-9]*$ + + + + + + +
+

+
Example:
+
[
+    "AHU-22"
+]
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ +

Discovery Model

Type: object
+

Discovery target parameters

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: object
+ + + + + + + +
+
+
+

+ +

+
+ +
+

+ +

+

All properties whose name matches the following regular expression must respect the following conditions

+ Property name regular expression: ^[a-z][a-z0-9]*(_[a-z0-9]+)*$ +
+ +

Family Discovery Model

Type: object
+ + + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: stringFormat: date-time
+

Generational marker for controlling discovery

+
+ + + + + +
+
Example:
+
"2018-08-26T21:00:13Z"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: integer
+

Period, in seconds, for automatic scanning

+
+ + + +

Value must be greater or equal to 0

+ + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: integer
+

Scan duration, in seconds

+
+ + + +

Value must be greater or equal to 0

+ + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ +

Localnet Model

Type: object
+

Used to describe device local network parameters

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: object
+

Parent device to which the device is physically connected

+
No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+

ID of the parent device to which the device is physically connected

+
Must match regular expression: ^[A-Z]{2,6}-[1-9][0-9]*$ + + + + + +
+
Examples:
+
"DDC-1"
+
+
"CGW-2"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Connection family/protocol of the parent device to which the device is physically connected

+
+ + + + + +
+
Examples:
+
"bacnet_mstp"
+
+
"modbus_tcp"
+
+
"mqtt"
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+ No Additional Properties + + + + + +
+
Examples:
+
"bacnet"
+
+
"modbus"
+
+
"mbus"
+
+
"ether"
+
+
"ipv4"
+
+
"ipv6"
+
+
"iot"
+
+
+
+
+
+

+ +

+
+ +
+

+ +

+

All properties whose name matches the following regular expression must respect the following conditions

+ Property name regular expression: ^[a-z0-9-]+$ +
+ +

Family Localnet Model

Type: object
+

The type of network

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+

The address of a device on the fieldbus/local network

+
+ + + + + +
+
Examples:
+
"192.168.1.3"
+
+
"41489"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

The slave unit ID of the device for protocol families like modbus

+
+ + + + + +
+
Examples:
+
"1"
+
+
"255"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

The network designator for this device in the family-defined format

+
+ + + + + +
+
Examples:
+
"192.168.1.0/24"
+
+
"12732"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+ No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+

+ +

+

All properties whose name matches the following regular expression must respect the following conditions

+ Property name regular expression: ^[a-z_]+$ +
+ + Type: string
+ + + + + + + +
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Specifies that this is a shadow of the indicated device

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

The device id of the node's parent

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: boolean
+

If true, then the system should not try to validate the refs

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

The family designator, used only when the entry is not keyed in a family map

+
+ + + + + +
+
Examples:
+
"ipv4"
+
+
"bacnet"
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+ No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+

+ +

+

All properties whose name matches the following regular expression must respect the following conditions

+ Property name regular expression: ^[a-z][a-z0-9]*(_[a-z0-9]+)*$ +
+ +

Family Localnet Model

Type: object
+

The type of network

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+

The address of a device on the fieldbus/local network

+
+ + + + + +
+
Examples:
+
"192.168.1.3"
+
+
"41489"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

The slave unit ID of the device for protocol families like modbus

+
+ + + + + +
+
Examples:
+
"1"
+
+
"255"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

The network designator for this device in the family-defined format

+
+ + + + + +
+
Examples:
+
"192.168.1.0/24"
+
+
"12732"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+ No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+

+ +

+

All properties whose name matches the following regular expression must respect the following conditions

+ Property name regular expression: ^[a-z_]+$ +
+ + Type: string
+ + + + + + + +
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Specifies that this is a shadow of the indicated device

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

The device id of the node's parent

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: boolean
+

If true, then the system should not try to validate the refs

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

The family designator, used only when the entry is not keyed in a family map

+
+ + + + + +
+
Examples:
+
"ipv4"
+
+
"bacnet"
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ +

Testing Model

Type: object
+

Testing target parameters

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: boolean
+ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+ No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+

+ +

+

All properties whose name matches the following regular expression must respect the following conditions

+ Property name regular expression: ^[a-z][a-z0-9]*(_[a-z0-9]+)*$ +
+ +

Target Testing Model

Type: object
+ + + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Point name used for testing

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+

Value used for testing

+
+ + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+ No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+

+ +

+

All properties whose name matches the following regular expression must respect the following conditions

+ Property name regular expression: ^[a-z][a-z0-9]*(_[a-z0-9]+)*$ +
+ +

Blob Update Testing Model

Type: object
+ + + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Blob name for the payload

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

URL for OTA update

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

SHA256 hash of the payload

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Expected software version

+
+ + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ +

Testing Model

Type: object
+

Model of supported features

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+

+ +

+

All properties whose name matches the following regular expression must respect the following conditions

+ Property name regular expression: ^[._a-zA-Z]+$ +
+ +

Feature Discovery

Type: object
+

Object representation for for a single feature

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ +

FeatureStage

Type: enum (of string)
+

Stage of a feature implemenation

+
+ +
+

Must be one of:

+
  • "disabled"
  • "alpha"
  • "preview"
  • "beta"
  • "stable"
+
+ + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ +

Alarmset Model

Type: object
+

Alarmset representing the abstract system expectation for what the device should be doing, and how it should be configured and operated. This block specifies the expected alarms that a device holds

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: object
+

Information about a specific alarm name of the device.

+
No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+

+ +

+

All properties whose name matches the following regular expression must respect the following conditions

+ Property name regular expression: ^[a-z][a-z0-9]*(_[a-z0-9]+)*$ +
+ +

Alarm Alarmset Model

Type: object
+

Information about a specific alarm name of the device.

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Expected unit configuration for the alarm

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: enum (of string)
+

Expected data type for the alarm

+
+

Must be one of:

+
  • "string"
  • "boolean"
  • "number"
+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Detailed description of this alarm

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: boolean
+

Indicates if this alarm is writable (else read-only)

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+

Represents the expected baseline value of the alarm

+
+ + + + + +
+
Example:
+
22
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: number
+

Maximum deviation from baseline_value

+
+ + + + + +
+
Example:
+
2
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: enum (of string)
+

Expected state when baseline_value is set as the set_value for this alarm the config message

+
+

Must be one of:

+
  • "applied"
  • "updating"
  • "overridden"
  • "invalid"
  • "failure"
+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: number
+

Represents the lower bound of the error threshold for a alarm

+
+ + + + + +
+
Example:
+
0
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: number
+

Represents the upper bound of the error threshold for a alarm

+
+ + + + + +
+
Example:
+
100
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: integer
+

Represents the limit in seconds that a alarm can be unchanged for

+
+ + + + + +
+
Example:
+
3600
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: number
+

Triggering threshold for partial cov update publishing

+
+ + + + + +
+
Example:
+
1
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Mapping for the alarm to an internal resource (e.g. BACnet object reference)

+
+ + + + + +
+
Examples:
+
"AI3"
+
+
"400070"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+ No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+

+ +

+

All properties whose name matches the following regular expression must respect the following conditions

+ Property name regular expression: ^[a-z_]+$ +
+ + Type: string
+ + + + + + + +
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: array
+

Tags assosciated with the alarm

+
+ + + + + +

All items must be unique

No Additional Items

Each item of this array must be:

+
+
+ + + Type: object
+Must match regular expression: ^[a-z0-9]+$ + + + + + + +
+

+
Examples:
+
[
+    "lighting"
+]
+
+
[
+    "energy",
+    "hvac"
+]
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+

Collection of family alarm information

+
No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+

+ +

+

All properties whose name matches the following regular expression must respect the following conditions

+ Property name regular expression: ^[-_.:/a-zA-Z0-9]+$ +
+ +

Ref Discovery

Type: object
+

Object representation for for a single point reference discovery

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Point descriptor for this point

+
Must match regular expression: ^[a-z][a-z0-9]*(_[a-z0-9]+)*$ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Friendly name for the reference, if known

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: array of string
+

List of possible enumerated values for the point

+
+ + + + + + No Additional Items

Each item of this array must be:

+
+
+ + + Type: string
+ + + + + + + +
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Current or default unit for this point

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Family reference for structures

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Current or default type for this point

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: boolean
+

Indicates if this point is writable or not

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Human-readable description of this point

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ +

Entry

Type: object
+ + + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+

A human-readable one-line description of the entry

+
+ + + + + +
+
Example:
+
"Point is not writable"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

An optional extensive entry which can include more detail, e.g. a complete program stack-trace

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ +

Category

Type: string
+ + +

+ +

+
+ + + Type: object
+Must match regular expression: ^system\.base\.start$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^system\.base\.shutdown$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^system\.base\.ready$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^system\.config\.receive$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^system\.config\.parse$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^system\.config\.apply$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^system\.auth\.login$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^system\.auth\.logout$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^system\.auth\.fail$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^pointset\.point\.nominal$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^pointset\.point\.applied$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^pointset\.point\.updating$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^pointset\.point\.overridden$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^pointset\.point\.failure$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^pointset\.point\.invalid$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^localnet\.network$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^localnet\.network\.connect$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^gateway\.setup\.attach$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^gateway\.proxy\.target$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^gateway\.proxy\.connect$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^discovery\.family\.scan$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^discovery\.device\.enumerate$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^discovery\.point\.describe$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^mapping\.device\.apply$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^blobset\.blob$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^blobset\.blob\.update$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^blobset\.blob\.receive$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^blobset\.blob\.fetch$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^blobset\.blob\.parse$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^blobset\.blob\.apply$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^blobset\.blob\.abort$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^blobset\.blob\.rollback$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^validation\.device\.receive$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^validation\.device\.schema$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^validation\.device\.content$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^validation\.device\.multiple$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^validation\.device\.extra$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^validation\.summary\.report$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^validation\.feature\.sequence$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^validation\.feature\.schema$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^validation\.feature\.capability$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^device(\.[a-z]+((_[a-z]+))*)+$ + + + + + + +
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: stringFormat: date-time
+

FC 3339 UTC timestamp the condition was triggered, or most recently updated

+
+ + + + + +
+
Example:
+
"2018-08-26T21:39:28.364Z"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: integer
+

The status level should conform to the numerical Stackdriver LogEntry levels. The DEFAULT value of 0 is not allowed (lowest value is 100, maximum 800).

+
+ + + +

Value must be greater or equal to 100 and lesser or equal to 800 and a multiple of 1

+ +
+
Example:
+
600
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ +

Ancillary Properties

Type: object
+

Arbitrary blob of json associated with this point

+
+ + + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+ No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+

+ +

+

All properties whose name matches the following regular expression must respect the following conditions

+ Property name regular expression: ^[a-z_]+$ +
+ + Type: string
+ + + + + + + +
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+ + + + + + + +
+
+
+

+ +

+
+ +
+
+ +

Family Discovery

Type: object
+

Discovery information for a protocol family.

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Device addr in the namespace of the given family

+
Must match regular expression: ^[-_.:0-9A-Za-z]+$ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: integer
+

Port number for the family connection (e.g. UDP port for BACnet/IP)

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Point reference in the namespace of the given family

+
Must match regular expression: ^[-_.:/#0-9A-Za-z]+$ + + + + + + +
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+

Reference links to alternate families

+
No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+

+ +

+

All properties whose name matches the following regular expression must respect the following conditions

+ Property name regular expression: ^[a-z][a-z0-9]*(_[a-z0-9]+)*$ +
+ +

Family Discovery

Type: object
+

Discovery information for a protocol family.

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Device addr in the namespace of the given family

+
Must match regular expression: ^[-_.:0-9A-Za-z]+$ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: integer
+

Port number for the family connection (e.g. UDP port for BACnet/IP)

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Point reference in the namespace of the given family

+
Must match regular expression: ^[-_.:/#0-9A-Za-z]+$ + + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: boolean
+ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: boolean
+ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: integer
+

Minimum time between sample updates for the device (including complete and COV updates). Updates more frequent than this should be coalesced into one update.

+
+ + + +

Value must be greater or equal to 0 and lesser or equal to 86400

+ + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: integer
+

Maximum time between samples for the device to send out a complete update. It can send out updates more frequently than this. Default to 600.

+
+ + + +

Value must be greater or equal to 1 and lesser or equal to 86400

+ + +
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ +

Pointset Model

Type: object
+

Pointset representing the abstract system expectation for what the device should be doing, and how it should be configured and operated. This block specifies the expected points that a device holds

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: object
+

Information about a specific point name of the device.

+
No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+

+ +

+

All properties whose name matches the following regular expression must respect the following conditions

+ Property name regular expression: ^[a-z][a-z0-9]*(_[a-z0-9]+)*$ +
+ +

Point Pointset Model

Type: object
+

Information about a specific point name of the device.

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Expected unit configuration for the point

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Actual input unit that is sent from the device

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: enum (of string)
+

Expected data type for the point

+
+

Must be one of:

+
  • "string"
  • "boolean"
  • "number"
+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Detailed description of this point

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: boolean
+

Indicates if this point is writable (else read-only)

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+

Represents the expected baseline value of the point

+
+ + + + + +
+
Example:
+
22
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: number
+

Maximum deviation from baseline_value

+
+ + + + + +
+
Example:
+
2
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: enum (of string)
+

Expected state when baseline_value is set as the set_value for this point the config message

+
+

Must be one of:

+
  • "applied"
  • "updating"
  • "overridden"
  • "invalid"
  • "failure"
+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: number
+

Represents the lower bound of the error threshold for a point

+
+ + + + + +
+
Example:
+
0
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: number
+

Represents the upper bound of the error threshold for a point

+
+ + + + + +
+
Example:
+
100
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: integer
+

Represents the limit in seconds that a point can be unchanged for

+
+ + + + + +
+
Example:
+
3600
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: number
+

Triggering threshold for partial cov update publishing

+
+ + + + + +
+
Example:
+
1
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Mapping for the point to an internal resource (e.g. BACnet object reference)

+
+ + + + + +
+
Examples:
+
"AI3"
+
+
"400070"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Complete resolved URL of this point as determined by the gateway and ref blocks

+
+ + + + + +
+
Example:
+
"bacnet://1234/AV/1"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+ No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+

+ +

+

All properties whose name matches the following regular expression must respect the following conditions

+ Property name regular expression: ^[a-z_]+$ +
+ + Type: string
+ + + + + + + +
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: array
+

Tags assosciated with the point

+
+ + + + + +

All items must be unique

No Additional Items

Each item of this array must be:

+
+
+ + + Type: object
+Must match regular expression: ^[a-z0-9]+$ + + + + + + +
+

+
Examples:
+
[
+    "lighting"
+]
+
+
[
+    "energy",
+    "hvac"
+]
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+

Collection of family point information

+
No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+

+ +

+

All properties whose name matches the following regular expression must respect the following conditions

+ Property name regular expression: ^[-_.:/a-zA-Z0-9]+$ +
+ +

Ref Discovery

Type: object
+

Object representation for for a single point reference discovery

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Point descriptor for this point

+
Must match regular expression: ^[a-z][a-z0-9]*(_[a-z0-9]+)*$ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Friendly name for the reference, if known

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: array of string
+

List of possible enumerated values for the point

+
+ + + + + + No Additional Items

Each item of this array must be:

+
+
+ + + Type: string
+ + + + + + + +
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Current or default unit for this point

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Family reference for structures

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Current or default type for this point

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: boolean
+

Indicates if this point is writable or not

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Human-readable description of this point

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ +

Entry

Type: object
+ + + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+

A human-readable one-line description of the entry

+
+ + + + + +
+
Example:
+
"Point is not writable"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

An optional extensive entry which can include more detail, e.g. a complete program stack-trace

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ +

Category

Type: string
+ + +

+ +

+
+ + + Type: object
+Must match regular expression: ^system\.base\.start$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^system\.base\.shutdown$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^system\.base\.ready$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^system\.config\.receive$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^system\.config\.parse$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^system\.config\.apply$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^system\.auth\.login$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^system\.auth\.logout$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^system\.auth\.fail$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^pointset\.point\.nominal$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^pointset\.point\.applied$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^pointset\.point\.updating$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^pointset\.point\.overridden$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^pointset\.point\.failure$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^pointset\.point\.invalid$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^localnet\.network$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^localnet\.network\.connect$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^gateway\.setup\.attach$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^gateway\.proxy\.target$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^gateway\.proxy\.connect$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^discovery\.family\.scan$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^discovery\.device\.enumerate$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^discovery\.point\.describe$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^mapping\.device\.apply$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^blobset\.blob$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^blobset\.blob\.update$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^blobset\.blob\.receive$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^blobset\.blob\.fetch$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^blobset\.blob\.parse$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^blobset\.blob\.apply$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^blobset\.blob\.abort$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^blobset\.blob\.rollback$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^validation\.device\.receive$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^validation\.device\.schema$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^validation\.device\.content$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^validation\.device\.multiple$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^validation\.device\.extra$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^validation\.summary\.report$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^validation\.feature\.sequence$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^validation\.feature\.schema$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^validation\.feature\.capability$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^device(\.[a-z]+((_[a-z]+))*)+$ + + + + + + +
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: stringFormat: date-time
+

FC 3339 UTC timestamp the condition was triggered, or most recently updated

+
+ + + + + +
+
Example:
+
"2018-08-26T21:39:28.364Z"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: integer
+

The status level should conform to the numerical Stackdriver LogEntry levels. The DEFAULT value of 0 is not allowed (lowest value is 100, maximum 800).

+
+ + + +

Value must be greater or equal to 100 and lesser or equal to 800 and a multiple of 1

+ +
+
Example:
+
600
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ +

Ancillary Properties

Type: object
+

Arbitrary blob of json associated with this point

+
+ + + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+ No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+

+ +

+

All properties whose name matches the following regular expression must respect the following conditions

+ Property name regular expression: ^[a-z_]+$ +
+ + Type: string
+ + + + + + + +
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+ + + + + + + +
+
+
+

+ +

+
+ +
+
+ +

Family Discovery

Type: object
+

Discovery information for a protocol family.

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Device addr in the namespace of the given family

+
Must match regular expression: ^[-_.:0-9A-Za-z]+$ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: integer
+

Port number for the family connection (e.g. UDP port for BACnet/IP)

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Point reference in the namespace of the given family

+
Must match regular expression: ^[-_.:/#0-9A-Za-z]+$ + + + + + + +
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+

Reference links to alternate families

+
No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+

+ +

+

All properties whose name matches the following regular expression must respect the following conditions

+ Property name regular expression: ^[a-z][a-z0-9]*(_[a-z0-9]+)*$ +
+ +

Family Discovery

Type: object
+

Discovery information for a protocol family.

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Device addr in the namespace of the given family

+
Must match regular expression: ^[-_.:0-9A-Za-z]+$ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: integer
+

Port number for the family connection (e.g. UDP port for BACnet/IP)

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Point reference in the namespace of the given family

+
Must match regular expression: ^[-_.:/#0-9A-Za-z]+$ + + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Virtual equipment mapping linking this local point to a remote point in another device

+
Must match regular expression: ^[-0-9a-zA-Z$]+:[a-z0-9_]+$ + + + + + +
+
Example:
+
"VAV-3:supply_air_flowrate_sensor"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+

State value mapping for this point (e.g. 0.0: OFF)

+
+ + + + + + +
+
+
+

+ +

+
+ +
+

Each additional property must conform to the following schema

+ + Type: string
+ + + + + + + +
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ +

Building Translation

Type: object
+

Discovery result with implicit enumeration

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+

dotted path to present_value field

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+ + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+ + + + + + + +
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Actual input unit that is sent from the device

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+ + + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: boolean
+ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: boolean
+ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: integer
+

Minimum time between sample updates for the device (including complete and COV updates). Updates more frequent than this should be coalesced into one update.

+
+ + + +

Value must be greater or equal to 0 and lesser or equal to 86400

+ + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: integer
+

Maximum time between samples for the device to send out a complete update. It can send out updates more frequently than this. Default to 600.

+
+ + + +

Value must be greater or equal to 1 and lesser or equal to 86400

+ + +
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+ + + + + + + +
+
+
+

+ +

+
+ +
+

+ +

+

All properties whose name matches the following regular expression must respect the following conditions

+ Property name regular expression: ^[a-z][a-z0-9]*(_[a-z0-9]+)*$ +
+ +

Discovery Events

Type: object
+

Discovery result with implicit discovery

+
Same definition as discovery +
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ +

Policy Model

Type: object
+ + + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: array of string
+

An array of rule sets which are applicable to the device

+
+ + + + + + No Additional Items

Each item of this array must be:

+
+
+ + + Type: string
+Must match regular expression: ^[a-z_0-9]*$ + + + + + + +
+

+
Example:
+
[
+    "ip",
+    "mqtt"
+]
+
diff --git a/gencode/docs/events_discovery.html b/gencode/docs/events_discovery.html index 174c0a7c08..351352b419 100644 --- a/gencode/docs/events_discovery.html +++ b/gencode/docs/events_discovery.html @@ -2416,6 +2416,40 @@

+

+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: integer
+

Total number of records expected for this active scan generation

+
+ + + +

Value must be greater or equal to 0

+ +
@@ -19164,6 +19198,51046 @@

"1"
 
+
+
+
+
+
+
+ + + + + + + +
+
+
+

+ +

+
+ +
+
+ +

Metadata

Type: object
+

Device metadata model (for model discovery)

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: stringFormat: date-time
+

RFC 3339 timestamp UTC the data was generated

+
+ + + + + +
+
Example:
+
"2019-01-17T14:02:29.364Z"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Version of the UDMI schema for this file

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Original version of the UDMI schema for this file

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

DEPRECATED

+
Must match regular expression: ^[0-9a-z]{8}$ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ +

Model Operation

Type: enum (of string)
+

Used for model update operations (so not a model proper), e.g. DELETE or BIND operations

+
+ +
+

Must be one of:

+
  • "ERROR"
  • "CREATE"
  • "READ"
  • "REPLY"
  • "UPDATE"
  • "DELETE"
  • "MODIFY"
  • "PREVIEW"
  • "BIND"
  • "UNBIND"
  • "BOUND"
  • "ALLOW"
  • "BLOCK"
+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ +

Cloud Model

Type: object
+

Information specific to how the device communicates with the cloud.

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: integer
+

Function version of this request

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: enum (of string)
+

The key type used for cloud communication.

+
+

Must be one of:

+
  • "ES256"
  • "ES256_X509"
  • "RS256"
  • "RS256_X509"
+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: boolean
+

Indicates if the device private key is provided in the site model.

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: enum (of string)
+

The cloud resource type

+
+

Must be one of:

+
  • "DIRECT"
  • "GATEWAY"
  • "FUNNEL"
  • "PROXIED"
  • "REGISTRY"
+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ +

Gateway Model

Type: object
+

Gateway Documentation

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+

The device ID of the gateway the device is bound to

+
Must match regular expression: ^[A-Z]{2,6}-[1-9][0-9]*$ + + + + + +
+
Example:
+
"GAT-100"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ +

Family Localnet Model

Type: object
+

The type of network

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+

The address of a device on the fieldbus/local network

+
+ + + + + +
+
Examples:
+
"192.168.1.3"
+
+
"41489"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

The slave unit ID of the device for protocol families like modbus

+
+ + + + + +
+
Examples:
+
"1"
+
+
"255"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

The network designator for this device in the family-defined format

+
+ + + + + +
+
Examples:
+
"192.168.1.0/24"
+
+
"12732"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+ No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+

+ +

+

All properties whose name matches the following regular expression must respect the following conditions

+ Property name regular expression: ^[a-z_]+$ +
+ + Type: string
+ + + + + + + +
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Specifies that this is a shadow of the indicated device

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

The device id of the node's parent

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: boolean
+

If true, then the system should not try to validate the refs

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

The family designator, used only when the entry is not keyed in a family map

+
+ + + + + +
+
Examples:
+
"ipv4"
+
+
"bacnet"
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: array of string
+

An array of all the device IDs which are bound to the device

+
+ + + + + + No Additional Items

Each item of this array must be:

+
+
+ + + Type: string
+Must match regular expression: ^[A-Z]{2,6}-[1-9][0-9]*$ + + + + + + +
+

+
Example:
+
[
+    "AHU-22"
+]
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ +

Config Cloud Model

Type: object
+

Information specific to how the device communicates with the cloud.

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Config file to use. Within the config directory in the device's metadata directory

+
+ + + + + + +
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: boolean
+ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: stringFormat: date-time
+ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: stringFormat: date-time
+ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: stringFormat: date-time
+ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: stringFormat: date-time
+ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ +

Model Operation

Type: enum (of string)
+ + +
+

Must be one of:

+
  • "ERROR"
  • "CREATE"
  • "READ"
  • "REPLY"
  • "UPDATE"
  • "DELETE"
  • "MODIFY"
  • "PREVIEW"
  • "BIND"
  • "UNBIND"
  • "BOUND"
  • "ALLOW"
  • "BLOCK"
+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Strigified version of the metadata object, used for internal backend processing

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+

If operating on the entire registry, then this contains results for multiple devices (batched operation).

+
+ + + + + + +
+
+
+

+ +

+
+ +
+

+ +

+

All properties whose name matches the following regular expression must respect the following conditions

+ Property name regular expression: ^[-._a-zA-Z0-9]+$ +
+ + Type: object
+ + + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ +

System Model

Type: object
+

High-level system information about the device. System Model Documentation

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Friendly name describing this device

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Generic human-readable text describing the device

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Version of schema supported by the device

+
+ + + + + +
+
Example:
+
"1.5.0"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Aggregation of spaces, partial spaces or other zones that the device is serving

+
Must match regular expression: ^[-_a-zA-Z0-9]+$ + + + + + +
+
Examples:
+
"Zone1"
+
+
"Kitchen_101"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

The description of what kind of node this is.

+
Must match regular expression: ^[a-z][_a-z0-9]+$ + + + + + +
+
Examples:
+
"vitual_device"
+
+
"field_group"
+
+
"field_controller"
+
+
"network_scan_agent"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: array
+

Tags associated with the device

+
+ + + + + +

All items must be unique

No Additional Items

Each item of this array must be:

+
+
+ + + Type: object
+Must match regular expression: ^[a-z][_a-z0-9]+$ + + + + + + +
+

+
Examples:
+
[
+    "lighting"
+]
+
+
[
+    "energy",
+    "building",
+    "vendor_managed"
+]
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+

Properties of the expected physical location of the device

+
No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+

The site name according to the site model in which the device is installed in

+
Must match regular expression: ^[A-Z]{2}-[A-Z]{3,4}-[A-Z0-9]{2,9}$ + + + + + +
+
Example:
+
"US-SFO-XYY"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

The reference of the panel where the device is installed in

+
Must match regular expression: ^[-_a-zA-Z0-9]+$ + + + + + +
+
Examples:
+
"Panel123"
+
+
"CTRP-567"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+Must match regular expression: ^[A-Z0-9-]+$ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+Must match regular expression: ^[-_a-zA-Z0-9]+$ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Name of floor level

+
Must match regular expression: ^[A-Z0-9-]+$ + + + + + +
+
Examples:
+
"1"
+
+
"1A"
+
+
"G"
+
+
"B1M"
+
+
"RF"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: integer
+

Sequential integer representation for a floor, primarily for comparisons when non integer floors are used, e.g. 1 and 1M

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+ No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: number
+

The x coordinate of the device location in a project specific coordinate system

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: number
+

The y coordinate of the device location in a project specific coordinate system

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: number
+

The z (height) coordinate of the device location in a project specific coordinate system

+
+ + + + + + +
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+ No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: number
+

The latitude coordinate of the device in the WGS84 coordinate system

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: number
+

The longitude coordinate of the device in the WGS84 coordinate system

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: number
+

The altitude coordinate of the device in meters

+
+ + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

The serial number of the physical device

+
+ + + + + +
+
Examples:
+
"A1B2C3D4"
+
+
"00001"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ +

SystemHardware

Type: object
+

A collection of fields which describe the physical hardware of the device.

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+

The manufacturer of the device

+
+ + + + + +
+
Example:
+
"Google"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

The model of the device

+
+ + + + + +
+
Examples:
+
"Pixel 5"
+
+
"Pixel 6"
+
+
"Pixel 6 Pro"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

A stock keeping unit which identifies the unique composition of a device, for example those with different hardware configurations

+
+ + + + + +
+
Examples:
+
"GF5KQ"
+
+
"4215612"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Hardware revision of the device

+
+ + + + + +
+
Examples:
+
"A"
+
+
"B"
+
+
"1"
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+

A collection of items which can be used to describe version of software running on a device

+
No Additional Properties + + + + + +
+
Examples:
+
"firmware"
+
+
"os"
+
+
"driver"
+
+
+
+
+
+

+ +

+
+ +
+

+ +

+

All properties whose name matches the following regular expression must respect the following conditions

+ Property name regular expression: ^[a-z_]+$ +
+ + Type: string
+ + + + + + + +
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+

Information used to print a physical QR code label.

+
No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: object
+ No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+Must match regular expression: ^[a-z]+://[-0-9a-zA-Z_$]+$ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+Must match regular expression: ^[A-Z]{2}-[A-Z]{3,4}-[A-Z0-9]{2,9}$ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+Must match regular expression: ^[A-Z]{2,6}-[1-9][0-9]*$ + + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+ No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+

+ +

+

All properties whose name matches the following regular expression must respect the following conditions

+ Property name regular expression: ^[a-z_]+$ +
+ + Type: string
+ + + + + + + +
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: integer Default: 300
+

The minimum loglevel for reporting log messages below which log entries should not be sent. Default to 300.

+
+ + + +

Value must be greater or equal to 100 and lesser or equal to 800

+ + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: integer
+

The rate at which the system should send system event updates. 0 indicates no updates.

+
+ + + +

Value must be greater or equal to 0 and lesser or equal to 86400

+ + +
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ +

Externals Model

Type: object
+

Information specific to how the device links with external models

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+

+ +

+

All properties whose name matches the following regular expression must respect the following conditions

+ Property name regular expression: ^[a-z]+(_[a-z]+)*$ +
+ +

Link Externals Model

Type: object
+

Information about how this device links to a specific external model

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Id of this device in the external model namespace

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Type of this device in the external model namespace

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Etag for referencing this external entity

+
Must match regular expression: ^[0-9a-f]+$ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Descriptive label for this entity

+
+ + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ +

Entity Relationships

Type: object
+

Entity relationships to other devices, keyed by target device ID

+
+ + + + + + + + +
+
+
+

+ +

+
+ +
+

+ +

+

All properties whose name matches the following regular expression must respect the following conditions

+ Property name regular expression: ^[-0-9a-zA-Z]+$ +
+ + Type: object
+ + + + + + + +
+
+
+

+ +

+
+ +
+

+ +

+

All properties whose name matches the following regular expression must respect the following conditions

+ Property name regular expression: ^[-0-9a-zA-Z_]+$ +
+ + Type: array of object
+ + + + + + + No Additional Items

Each item of this array must be:

+
+
+ + + Type: object
+ + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: array of string
+ + + + + + + No Additional Items

Each item of this array must be:

+
+
+ + + Type: string
+ + + + + + + +
+

+
Example:
+
[
+    "primary"
+]
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ +

Gateway Model

Type: object
+

Gateway Documentation

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+

The device ID of the gateway the device is bound to

+
Must match regular expression: ^[A-Z]{2,6}-[1-9][0-9]*$ + + + + + +
+
Example:
+
"GAT-100"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ +

Family Localnet Model

Type: object
+

The type of network

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+

The address of a device on the fieldbus/local network

+
+ + + + + +
+
Examples:
+
"192.168.1.3"
+
+
"41489"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

The slave unit ID of the device for protocol families like modbus

+
+ + + + + +
+
Examples:
+
"1"
+
+
"255"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

The network designator for this device in the family-defined format

+
+ + + + + +
+
Examples:
+
"192.168.1.0/24"
+
+
"12732"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+ No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+

+ +

+

All properties whose name matches the following regular expression must respect the following conditions

+ Property name regular expression: ^[a-z_]+$ +
+ + Type: string
+ + + + + + + +
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Specifies that this is a shadow of the indicated device

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

The device id of the node's parent

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: boolean
+

If true, then the system should not try to validate the refs

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

The family designator, used only when the entry is not keyed in a family map

+
+ + + + + +
+
Examples:
+
"ipv4"
+
+
"bacnet"
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: array of string
+

An array of all the device IDs which are bound to the device

+
+ + + + + + No Additional Items

Each item of this array must be:

+
+
+ + + Type: string
+Must match regular expression: ^[A-Z]{2,6}-[1-9][0-9]*$ + + + + + + +
+

+
Example:
+
[
+    "AHU-22"
+]
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ +

Discovery Model

Type: object
+

Discovery target parameters

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: object
+ + + + + + + +
+
+
+

+ +

+
+ +
+

+ +

+

All properties whose name matches the following regular expression must respect the following conditions

+ Property name regular expression: ^[a-z][a-z0-9]*(_[a-z0-9]+)*$ +
+ +

Family Discovery Model

Type: object
+ + + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: stringFormat: date-time
+

Generational marker for controlling discovery

+
+ + + + + +
+
Example:
+
"2018-08-26T21:00:13Z"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: integer
+

Period, in seconds, for automatic scanning

+
+ + + +

Value must be greater or equal to 0

+ + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: integer
+

Scan duration, in seconds

+
+ + + +

Value must be greater or equal to 0

+ + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ +

Localnet Model

Type: object
+

Used to describe device local network parameters

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: object
+

Parent device to which the device is physically connected

+
No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+

ID of the parent device to which the device is physically connected

+
Must match regular expression: ^[A-Z]{2,6}-[1-9][0-9]*$ + + + + + +
+
Examples:
+
"DDC-1"
+
+
"CGW-2"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Connection family/protocol of the parent device to which the device is physically connected

+
+ + + + + +
+
Examples:
+
"bacnet_mstp"
+
+
"modbus_tcp"
+
+
"mqtt"
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+ No Additional Properties + + + + + +
+
Examples:
+
"bacnet"
+
+
"modbus"
+
+
"mbus"
+
+
"ether"
+
+
"ipv4"
+
+
"ipv6"
+
+
"iot"
+
+
+
+
+
+

+ +

+
+ +
+

+ +

+

All properties whose name matches the following regular expression must respect the following conditions

+ Property name regular expression: ^[a-z0-9-]+$ +
+ +

Family Localnet Model

Type: object
+

The type of network

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+

The address of a device on the fieldbus/local network

+
+ + + + + +
+
Examples:
+
"192.168.1.3"
+
+
"41489"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

The slave unit ID of the device for protocol families like modbus

+
+ + + + + +
+
Examples:
+
"1"
+
+
"255"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

The network designator for this device in the family-defined format

+
+ + + + + +
+
Examples:
+
"192.168.1.0/24"
+
+
"12732"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+ No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+

+ +

+

All properties whose name matches the following regular expression must respect the following conditions

+ Property name regular expression: ^[a-z_]+$ +
+ + Type: string
+ + + + + + + +
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Specifies that this is a shadow of the indicated device

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

The device id of the node's parent

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: boolean
+

If true, then the system should not try to validate the refs

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

The family designator, used only when the entry is not keyed in a family map

+
+ + + + + +
+
Examples:
+
"ipv4"
+
+
"bacnet"
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+ No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+

+ +

+

All properties whose name matches the following regular expression must respect the following conditions

+ Property name regular expression: ^[a-z][a-z0-9]*(_[a-z0-9]+)*$ +
+ +

Family Localnet Model

Type: object
+

The type of network

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+

The address of a device on the fieldbus/local network

+
+ + + + + +
+
Examples:
+
"192.168.1.3"
+
+
"41489"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

The slave unit ID of the device for protocol families like modbus

+
+ + + + + +
+
Examples:
+
"1"
+
+
"255"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

The network designator for this device in the family-defined format

+
+ + + + + +
+
Examples:
+
"192.168.1.0/24"
+
+
"12732"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+ No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+

+ +

+

All properties whose name matches the following regular expression must respect the following conditions

+ Property name regular expression: ^[a-z_]+$ +
+ + Type: string
+ + + + + + + +
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Specifies that this is a shadow of the indicated device

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

The device id of the node's parent

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: boolean
+

If true, then the system should not try to validate the refs

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

The family designator, used only when the entry is not keyed in a family map

+
+ + + + + +
+
Examples:
+
"ipv4"
+
+
"bacnet"
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ +

Testing Model

Type: object
+

Testing target parameters

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: boolean
+ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+ No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+

+ +

+

All properties whose name matches the following regular expression must respect the following conditions

+ Property name regular expression: ^[a-z][a-z0-9]*(_[a-z0-9]+)*$ +
+ +

Target Testing Model

Type: object
+ + + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Point name used for testing

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+

Value used for testing

+
+ + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+ No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+

+ +

+

All properties whose name matches the following regular expression must respect the following conditions

+ Property name regular expression: ^[a-z][a-z0-9]*(_[a-z0-9]+)*$ +
+ +

Blob Update Testing Model

Type: object
+ + + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Blob name for the payload

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

URL for OTA update

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

SHA256 hash of the payload

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Expected software version

+
+ + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ +

Testing Model

Type: object
+

Model of supported features

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+

+ +

+

All properties whose name matches the following regular expression must respect the following conditions

+ Property name regular expression: ^[._a-zA-Z]+$ +
+ +

Feature Discovery

Type: object
+

Object representation for for a single feature

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ +

FeatureStage

Type: enum (of string)
+

Stage of a feature implemenation

+
+ +
+

Must be one of:

+
  • "disabled"
  • "alpha"
  • "preview"
  • "beta"
  • "stable"
+
+ + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ +

Alarmset Model

Type: object
+

Alarmset representing the abstract system expectation for what the device should be doing, and how it should be configured and operated. This block specifies the expected alarms that a device holds

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: object
+

Information about a specific alarm name of the device.

+
No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+

+ +

+

All properties whose name matches the following regular expression must respect the following conditions

+ Property name regular expression: ^[a-z][a-z0-9]*(_[a-z0-9]+)*$ +
+ +

Alarm Alarmset Model

Type: object
+

Information about a specific alarm name of the device.

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Expected unit configuration for the alarm

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: enum (of string)
+

Expected data type for the alarm

+
+

Must be one of:

+
  • "string"
  • "boolean"
  • "number"
+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Detailed description of this alarm

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: boolean
+

Indicates if this alarm is writable (else read-only)

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+

Represents the expected baseline value of the alarm

+
+ + + + + +
+
Example:
+
22
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: number
+

Maximum deviation from baseline_value

+
+ + + + + +
+
Example:
+
2
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: enum (of string)
+

Expected state when baseline_value is set as the set_value for this alarm the config message

+
+

Must be one of:

+
  • "applied"
  • "updating"
  • "overridden"
  • "invalid"
  • "failure"
+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: number
+

Represents the lower bound of the error threshold for a alarm

+
+ + + + + +
+
Example:
+
0
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: number
+

Represents the upper bound of the error threshold for a alarm

+
+ + + + + +
+
Example:
+
100
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: integer
+

Represents the limit in seconds that a alarm can be unchanged for

+
+ + + + + +
+
Example:
+
3600
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: number
+

Triggering threshold for partial cov update publishing

+
+ + + + + +
+
Example:
+
1
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Mapping for the alarm to an internal resource (e.g. BACnet object reference)

+
+ + + + + +
+
Examples:
+
"AI3"
+
+
"400070"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+ No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+

+ +

+

All properties whose name matches the following regular expression must respect the following conditions

+ Property name regular expression: ^[a-z_]+$ +
+ + Type: string
+ + + + + + + +
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: array
+

Tags assosciated with the alarm

+
+ + + + + +

All items must be unique

No Additional Items

Each item of this array must be:

+
+
+ + + Type: object
+Must match regular expression: ^[a-z0-9]+$ + + + + + + +
+

+
Examples:
+
[
+    "lighting"
+]
+
+
[
+    "energy",
+    "hvac"
+]
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+

Collection of family alarm information

+
No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+

+ +

+

All properties whose name matches the following regular expression must respect the following conditions

+ Property name regular expression: ^[-_.:/a-zA-Z0-9]+$ +
+ +

Ref Discovery

Type: object
+

Object representation for for a single point reference discovery

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Point descriptor for this point

+
Must match regular expression: ^[a-z][a-z0-9]*(_[a-z0-9]+)*$ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Friendly name for the reference, if known

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: array of string
+

List of possible enumerated values for the point

+
+ + + + + + No Additional Items

Each item of this array must be:

+
+
+ + + Type: string
+ + + + + + + +
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Current or default unit for this point

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Family reference for structures

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Current or default type for this point

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: boolean
+

Indicates if this point is writable or not

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Human-readable description of this point

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ +

Entry

Type: object
+ + + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+

A human-readable one-line description of the entry

+
+ + + + + +
+
Example:
+
"Point is not writable"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

An optional extensive entry which can include more detail, e.g. a complete program stack-trace

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ +

Category

Type: string
+ + +

+ +

+
+ + + Type: object
+Must match regular expression: ^system\.base\.start$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^system\.base\.shutdown$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^system\.base\.ready$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^system\.config\.receive$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^system\.config\.parse$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^system\.config\.apply$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^system\.auth\.login$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^system\.auth\.logout$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^system\.auth\.fail$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^pointset\.point\.nominal$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^pointset\.point\.applied$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^pointset\.point\.updating$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^pointset\.point\.overridden$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^pointset\.point\.failure$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^pointset\.point\.invalid$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^localnet\.network$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^localnet\.network\.connect$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^gateway\.setup\.attach$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^gateway\.proxy\.target$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^gateway\.proxy\.connect$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^discovery\.family\.scan$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^discovery\.device\.enumerate$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^discovery\.point\.describe$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^mapping\.device\.apply$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^blobset\.blob$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^blobset\.blob\.update$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^blobset\.blob\.receive$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^blobset\.blob\.fetch$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^blobset\.blob\.parse$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^blobset\.blob\.apply$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^blobset\.blob\.abort$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^blobset\.blob\.rollback$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^validation\.device\.receive$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^validation\.device\.schema$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^validation\.device\.content$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^validation\.device\.multiple$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^validation\.device\.extra$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^validation\.summary\.report$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^validation\.feature\.sequence$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^validation\.feature\.schema$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^validation\.feature\.capability$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^device(\.[a-z]+((_[a-z]+))*)+$ + + + + + + +
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: stringFormat: date-time
+

FC 3339 UTC timestamp the condition was triggered, or most recently updated

+
+ + + + + +
+
Example:
+
"2018-08-26T21:39:28.364Z"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: integer
+

The status level should conform to the numerical Stackdriver LogEntry levels. The DEFAULT value of 0 is not allowed (lowest value is 100, maximum 800).

+
+ + + +

Value must be greater or equal to 100 and lesser or equal to 800 and a multiple of 1

+ +
+
Example:
+
600
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ +

Ancillary Properties

Type: object
+

Arbitrary blob of json associated with this point

+
+ + + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+ No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+

+ +

+

All properties whose name matches the following regular expression must respect the following conditions

+ Property name regular expression: ^[a-z_]+$ +
+ + Type: string
+ + + + + + + +
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+ + + + + + + +
+
+
+

+ +

+
+ +
+
+ +

Family Discovery

Type: object
+

Discovery information for a protocol family.

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Device addr in the namespace of the given family

+
Must match regular expression: ^[-_.:0-9A-Za-z]+$ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: integer
+

Port number for the family connection (e.g. UDP port for BACnet/IP)

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Point reference in the namespace of the given family

+
Must match regular expression: ^[-_.:/#0-9A-Za-z]+$ + + + + + + +
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+

Reference links to alternate families

+
No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+

+ +

+

All properties whose name matches the following regular expression must respect the following conditions

+ Property name regular expression: ^[a-z][a-z0-9]*(_[a-z0-9]+)*$ +
+ +

Family Discovery

Type: object
+

Discovery information for a protocol family.

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Device addr in the namespace of the given family

+
Must match regular expression: ^[-_.:0-9A-Za-z]+$ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: integer
+

Port number for the family connection (e.g. UDP port for BACnet/IP)

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Point reference in the namespace of the given family

+
Must match regular expression: ^[-_.:/#0-9A-Za-z]+$ + + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: boolean
+ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: boolean
+ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: integer
+

Minimum time between sample updates for the device (including complete and COV updates). Updates more frequent than this should be coalesced into one update.

+
+ + + +

Value must be greater or equal to 0 and lesser or equal to 86400

+ + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: integer
+

Maximum time between samples for the device to send out a complete update. It can send out updates more frequently than this. Default to 600.

+
+ + + +

Value must be greater or equal to 1 and lesser or equal to 86400

+ + +
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ +

Pointset Model

Type: object
+

Pointset representing the abstract system expectation for what the device should be doing, and how it should be configured and operated. This block specifies the expected points that a device holds

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: object
+

Information about a specific point name of the device.

+
No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+

+ +

+

All properties whose name matches the following regular expression must respect the following conditions

+ Property name regular expression: ^[a-z][a-z0-9]*(_[a-z0-9]+)*$ +
+ +

Point Pointset Model

Type: object
+

Information about a specific point name of the device.

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Expected unit configuration for the point

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Actual input unit that is sent from the device

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: enum (of string)
+

Expected data type for the point

+
+

Must be one of:

+
  • "string"
  • "boolean"
  • "number"
+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Detailed description of this point

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: boolean
+

Indicates if this point is writable (else read-only)

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+

Represents the expected baseline value of the point

+
+ + + + + +
+
Example:
+
22
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: number
+

Maximum deviation from baseline_value

+
+ + + + + +
+
Example:
+
2
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: enum (of string)
+

Expected state when baseline_value is set as the set_value for this point the config message

+
+

Must be one of:

+
  • "applied"
  • "updating"
  • "overridden"
  • "invalid"
  • "failure"
+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: number
+

Represents the lower bound of the error threshold for a point

+
+ + + + + +
+
Example:
+
0
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: number
+

Represents the upper bound of the error threshold for a point

+
+ + + + + +
+
Example:
+
100
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: integer
+

Represents the limit in seconds that a point can be unchanged for

+
+ + + + + +
+
Example:
+
3600
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: number
+

Triggering threshold for partial cov update publishing

+
+ + + + + +
+
Example:
+
1
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Mapping for the point to an internal resource (e.g. BACnet object reference)

+
+ + + + + +
+
Examples:
+
"AI3"
+
+
"400070"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Complete resolved URL of this point as determined by the gateway and ref blocks

+
+ + + + + +
+
Example:
+
"bacnet://1234/AV/1"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+ No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+

+ +

+

All properties whose name matches the following regular expression must respect the following conditions

+ Property name regular expression: ^[a-z_]+$ +
+ + Type: string
+ + + + + + + +
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: array
+

Tags assosciated with the point

+
+ + + + + +

All items must be unique

No Additional Items

Each item of this array must be:

+
+
+ + + Type: object
+Must match regular expression: ^[a-z0-9]+$ + + + + + + +
+

+
Examples:
+
[
+    "lighting"
+]
+
+
[
+    "energy",
+    "hvac"
+]
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+

Collection of family point information

+
No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+

+ +

+

All properties whose name matches the following regular expression must respect the following conditions

+ Property name regular expression: ^[-_.:/a-zA-Z0-9]+$ +
+ +

Ref Discovery

Type: object
+

Object representation for for a single point reference discovery

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Point descriptor for this point

+
Must match regular expression: ^[a-z][a-z0-9]*(_[a-z0-9]+)*$ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Friendly name for the reference, if known

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: array of string
+

List of possible enumerated values for the point

+
+ + + + + + No Additional Items

Each item of this array must be:

+
+
+ + + Type: string
+ + + + + + + +
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Current or default unit for this point

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Family reference for structures

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Current or default type for this point

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: boolean
+

Indicates if this point is writable or not

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Human-readable description of this point

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ +

Entry

Type: object
+ + + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+

A human-readable one-line description of the entry

+
+ + + + + +
+
Example:
+
"Point is not writable"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

An optional extensive entry which can include more detail, e.g. a complete program stack-trace

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ +

Category

Type: string
+ + +

+ +

+
+ + + Type: object
+Must match regular expression: ^system\.base\.start$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^system\.base\.shutdown$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^system\.base\.ready$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^system\.config\.receive$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^system\.config\.parse$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^system\.config\.apply$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^system\.auth\.login$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^system\.auth\.logout$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^system\.auth\.fail$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^pointset\.point\.nominal$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^pointset\.point\.applied$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^pointset\.point\.updating$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^pointset\.point\.overridden$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^pointset\.point\.failure$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^pointset\.point\.invalid$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^localnet\.network$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^localnet\.network\.connect$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^gateway\.setup\.attach$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^gateway\.proxy\.target$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^gateway\.proxy\.connect$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^discovery\.family\.scan$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^discovery\.device\.enumerate$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^discovery\.point\.describe$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^mapping\.device\.apply$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^blobset\.blob$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^blobset\.blob\.update$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^blobset\.blob\.receive$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^blobset\.blob\.fetch$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^blobset\.blob\.parse$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^blobset\.blob\.apply$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^blobset\.blob\.abort$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^blobset\.blob\.rollback$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^validation\.device\.receive$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^validation\.device\.schema$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^validation\.device\.content$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^validation\.device\.multiple$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^validation\.device\.extra$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^validation\.summary\.report$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^validation\.feature\.sequence$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^validation\.feature\.schema$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^validation\.feature\.capability$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^device(\.[a-z]+((_[a-z]+))*)+$ + + + + + + +
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: stringFormat: date-time
+

FC 3339 UTC timestamp the condition was triggered, or most recently updated

+
+ + + + + +
+
Example:
+
"2018-08-26T21:39:28.364Z"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: integer
+

The status level should conform to the numerical Stackdriver LogEntry levels. The DEFAULT value of 0 is not allowed (lowest value is 100, maximum 800).

+
+ + + +

Value must be greater or equal to 100 and lesser or equal to 800 and a multiple of 1

+ +
+
Example:
+
600
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ +

Ancillary Properties

Type: object
+

Arbitrary blob of json associated with this point

+
+ + + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+ No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+

+ +

+

All properties whose name matches the following regular expression must respect the following conditions

+ Property name regular expression: ^[a-z_]+$ +
+ + Type: string
+ + + + + + + +
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+ + + + + + + +
+
+
+

+ +

+
+ +
+
+ +

Family Discovery

Type: object
+

Discovery information for a protocol family.

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Device addr in the namespace of the given family

+
Must match regular expression: ^[-_.:0-9A-Za-z]+$ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: integer
+

Port number for the family connection (e.g. UDP port for BACnet/IP)

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Point reference in the namespace of the given family

+
Must match regular expression: ^[-_.:/#0-9A-Za-z]+$ + + + + + + +
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+

Reference links to alternate families

+
No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+

+ +

+

All properties whose name matches the following regular expression must respect the following conditions

+ Property name regular expression: ^[a-z][a-z0-9]*(_[a-z0-9]+)*$ +
+ +

Family Discovery

Type: object
+

Discovery information for a protocol family.

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Device addr in the namespace of the given family

+
Must match regular expression: ^[-_.:0-9A-Za-z]+$ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: integer
+

Port number for the family connection (e.g. UDP port for BACnet/IP)

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Point reference in the namespace of the given family

+
Must match regular expression: ^[-_.:/#0-9A-Za-z]+$ + + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Virtual equipment mapping linking this local point to a remote point in another device

+
Must match regular expression: ^[-0-9a-zA-Z$]+:[a-z0-9_]+$ + + + + + +
+
Example:
+
"VAV-3:supply_air_flowrate_sensor"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+

State value mapping for this point (e.g. 0.0: OFF)

+
+ + + + + + +
+
+
+

+ +

+
+ +
+

Each additional property must conform to the following schema

+ + Type: string
+ + + + + + + +
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ +

Building Translation

Type: object
+

Discovery result with implicit enumeration

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+

dotted path to present_value field

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+ + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+ + + + + + + +
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Actual input unit that is sent from the device

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+ + + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: boolean
+ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: boolean
+ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: integer
+

Minimum time between sample updates for the device (including complete and COV updates). Updates more frequent than this should be coalesced into one update.

+
+ + + +

Value must be greater or equal to 0 and lesser or equal to 86400

+ + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: integer
+

Maximum time between samples for the device to send out a complete update. It can send out updates more frequently than this. Default to 600.

+
+ + + +

Value must be greater or equal to 1 and lesser or equal to 86400

+ + +
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+ + + + + + + +
+
+
+

+ +

+
+ +
+

+ +

+

All properties whose name matches the following regular expression must respect the following conditions

+ Property name regular expression: ^[a-z][a-z0-9]*(_[a-z0-9]+)*$ +
+ +

Discovery Events

Type: object
+

Discovery result with implicit discovery

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: stringFormat: date-time
+

RFC 3339 UTC timestamp the discover telemetry event was generated

+
+ + + + + +
+
Example:
+
"2019-01-17T14:02:29.364Z"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Version of the UDMI schema

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: stringFormat: date-time
+

The event's discovery scan trigger's generation timestamp

+
+ + + + + +
+
Example:
+
"2019-01-17T14:02:29.364Z"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ +

Entry

Type: object
+ + + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+

A human-readable one-line description of the entry

+
+ + + + + +
+
Example:
+
"Point is not writable"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

An optional extensive entry which can include more detail, e.g. a complete program stack-trace

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ +

Category

Type: string
+ + +

+ +

+
+ + + Type: object
+Must match regular expression: ^system\.base\.start$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^system\.base\.shutdown$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^system\.base\.ready$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^system\.config\.receive$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^system\.config\.parse$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^system\.config\.apply$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^system\.auth\.login$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^system\.auth\.logout$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^system\.auth\.fail$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^pointset\.point\.nominal$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^pointset\.point\.applied$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^pointset\.point\.updating$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^pointset\.point\.overridden$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^pointset\.point\.failure$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^pointset\.point\.invalid$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^localnet\.network$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^localnet\.network\.connect$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^gateway\.setup\.attach$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^gateway\.proxy\.target$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^gateway\.proxy\.connect$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^discovery\.family\.scan$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^discovery\.device\.enumerate$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^discovery\.point\.describe$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^mapping\.device\.apply$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^blobset\.blob$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^blobset\.blob\.update$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^blobset\.blob\.receive$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^blobset\.blob\.fetch$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^blobset\.blob\.parse$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^blobset\.blob\.apply$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^blobset\.blob\.abort$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^blobset\.blob\.rollback$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^validation\.device\.receive$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^validation\.device\.schema$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^validation\.device\.content$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^validation\.device\.multiple$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^validation\.device\.extra$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^validation\.summary\.report$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^validation\.feature\.sequence$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^validation\.feature\.schema$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^validation\.feature\.capability$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^device(\.[a-z]+((_[a-z]+))*)+$ + + + + + + +
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: stringFormat: date-time
+

FC 3339 UTC timestamp the condition was triggered, or most recently updated

+
+ + + + + +
+
Example:
+
"2018-08-26T21:39:28.364Z"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: integer
+

The status level should conform to the numerical Stackdriver LogEntry levels. The DEFAULT value of 0 is not allowed (lowest value is 100, maximum 800).

+
+ + + +

Value must be greater or equal to 100 and lesser or equal to 800 and a multiple of 1

+ +
+
Example:
+
600
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

The primary address of the device (for indicated family)

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

The family network designator of the discovered device

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: integer
+

The active or passive series number of this result (matches reported state values)

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: integer
+

Total number of records expected for this active scan generation

+
+ + + +

Value must be greater or equal to 0

+ + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+

Links to other address families (family and id)

+
No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+

+ +

+

All properties whose name matches the following regular expression must respect the following conditions

+ Property name regular expression: ^[a-z][a-z0-9]*(_[a-z0-9]+)*$ +
+ +

Family Discovery

Type: object
+

Discovery information for a protocol family.

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Device addr in the namespace of the given family

+
Must match regular expression: ^[-_.:0-9A-Za-z]+$ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: integer
+

Port number for the family connection (e.g. UDP port for BACnet/IP)

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Point reference in the namespace of the given family

+
Must match regular expression: ^[-_.:/#0-9A-Za-z]+$ + + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+

Registry iot cloud models

+
No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+

+ +

+

All properties whose name matches the following regular expression must respect the following conditions

+ Property name regular expression: ^[A-Z]{2,6}-[1-9][0-9]*$ +
+ +

Cloud Model

Type: object
+

Information specific to how the device communicates with the cloud.

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: integer
+

Function version of this request

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: enum (of string)
+

The key type used for cloud communication.

+
+

Must be one of:

+
  • "ES256"
  • "ES256_X509"
  • "RS256"
  • "RS256_X509"
+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: boolean
+

Indicates if the device private key is provided in the site model.

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: enum (of string)
+

The cloud resource type

+
+

Must be one of:

+
  • "DIRECT"
  • "GATEWAY"
  • "FUNNEL"
  • "PROXIED"
  • "REGISTRY"
+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ +

Gateway Model

Type: object
+

Gateway Documentation

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+

The device ID of the gateway the device is bound to

+
Must match regular expression: ^[A-Z]{2,6}-[1-9][0-9]*$ + + + + + +
+
Example:
+
"GAT-100"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ +

Family Localnet Model

Type: object
+

The type of network

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+

The address of a device on the fieldbus/local network

+
+ + + + + +
+
Examples:
+
"192.168.1.3"
+
+
"41489"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

The slave unit ID of the device for protocol families like modbus

+
+ + + + + +
+
Examples:
+
"1"
+
+
"255"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

The network designator for this device in the family-defined format

+
+ + + + + +
+
Examples:
+
"192.168.1.0/24"
+
+
"12732"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+ No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+

+ +

+

All properties whose name matches the following regular expression must respect the following conditions

+ Property name regular expression: ^[a-z_]+$ +
+ + Type: string
+ + + + + + + +
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Specifies that this is a shadow of the indicated device

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

The device id of the node's parent

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: boolean
+

If true, then the system should not try to validate the refs

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

The family designator, used only when the entry is not keyed in a family map

+
+ + + + + +
+
Examples:
+
"ipv4"
+
+
"bacnet"
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: array of string
+

An array of all the device IDs which are bound to the device

+
+ + + + + + No Additional Items

Each item of this array must be:

+
+
+ + + Type: string
+Must match regular expression: ^[A-Z]{2,6}-[1-9][0-9]*$ + + + + + + +
+

+
Example:
+
[
+    "AHU-22"
+]
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ +

Config Cloud Model

Type: object
+

Information specific to how the device communicates with the cloud.

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Config file to use. Within the config directory in the device's metadata directory

+
+ + + + + + +
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: boolean
+ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: stringFormat: date-time
+ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: stringFormat: date-time
+ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: stringFormat: date-time
+ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: stringFormat: date-time
+ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ +

Model Operation

Type: enum (of string)
+ + +
+

Must be one of:

+
  • "ERROR"
  • "CREATE"
  • "READ"
  • "REPLY"
  • "UPDATE"
  • "DELETE"
  • "MODIFY"
  • "PREVIEW"
  • "BIND"
  • "UNBIND"
  • "BOUND"
  • "ALLOW"
  • "BLOCK"
+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Strigified version of the metadata object, used for internal backend processing

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+

If operating on the entire registry, then this contains results for multiple devices (batched operation).

+
+ + + + + + +
+
+
+

+ +

+
+ +
+

+ +

+

All properties whose name matches the following regular expression must respect the following conditions

+ Property name regular expression: ^[-._a-zA-Z0-9]+$ +
+ + Type: object
+ + + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+

Device iot cloud models

+
No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+

+ +

+

All properties whose name matches the following regular expression must respect the following conditions

+ Property name regular expression: ^[A-Z]{2,6}-[1-9][0-9]*$ +
+ +

Cloud Model

Type: object
+

Information specific to how the device communicates with the cloud.

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: integer
+

Function version of this request

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: enum (of string)
+

The key type used for cloud communication.

+
+

Must be one of:

+
  • "ES256"
  • "ES256_X509"
  • "RS256"
  • "RS256_X509"
+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: boolean
+

Indicates if the device private key is provided in the site model.

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: enum (of string)
+

The cloud resource type

+
+

Must be one of:

+
  • "DIRECT"
  • "GATEWAY"
  • "FUNNEL"
  • "PROXIED"
  • "REGISTRY"
+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ +

Gateway Model

Type: object
+

Gateway Documentation

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+

The device ID of the gateway the device is bound to

+
Must match regular expression: ^[A-Z]{2,6}-[1-9][0-9]*$ + + + + + +
+
Example:
+
"GAT-100"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ +

Family Localnet Model

Type: object
+

The type of network

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+

The address of a device on the fieldbus/local network

+
+ + + + + +
+
Examples:
+
"192.168.1.3"
+
+
"41489"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

The slave unit ID of the device for protocol families like modbus

+
+ + + + + +
+
Examples:
+
"1"
+
+
"255"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

The network designator for this device in the family-defined format

+
+ + + + + +
+
Examples:
+
"192.168.1.0/24"
+
+
"12732"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+ No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+

+ +

+

All properties whose name matches the following regular expression must respect the following conditions

+ Property name regular expression: ^[a-z_]+$ +
+ + Type: string
+ + + + + + + +
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Specifies that this is a shadow of the indicated device

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

The device id of the node's parent

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: boolean
+

If true, then the system should not try to validate the refs

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

The family designator, used only when the entry is not keyed in a family map

+
+ + + + + +
+
Examples:
+
"ipv4"
+
+
"bacnet"
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: array of string
+

An array of all the device IDs which are bound to the device

+
+ + + + + + No Additional Items

Each item of this array must be:

+
+
+ + + Type: string
+Must match regular expression: ^[A-Z]{2,6}-[1-9][0-9]*$ + + + + + + +
+

+
Example:
+
[
+    "AHU-22"
+]
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ +

Config Cloud Model

Type: object
+

Information specific to how the device communicates with the cloud.

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Config file to use. Within the config directory in the device's metadata directory

+
+ + + + + + +
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: boolean
+ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: stringFormat: date-time
+ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: stringFormat: date-time
+ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: stringFormat: date-time
+ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: stringFormat: date-time
+ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ +

Model Operation

Type: enum (of string)
+ + +
+

Must be one of:

+
  • "ERROR"
  • "CREATE"
  • "READ"
  • "REPLY"
  • "UPDATE"
  • "DELETE"
  • "MODIFY"
  • "PREVIEW"
  • "BIND"
  • "UNBIND"
  • "BOUND"
  • "ALLOW"
  • "BLOCK"
+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Strigified version of the metadata object, used for internal backend processing

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+

If operating on the entire registry, then this contains results for multiple devices (batched operation).

+
+ + + + + + +
+
+
+

+ +

+
+ +
+

+ +

+

All properties whose name matches the following regular expression must respect the following conditions

+ Property name regular expression: ^[-._a-zA-Z0-9]+$ +
+ + Type: object
+ + + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+

Information about a specific point name of the device.

+
No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+

+ +

+

All properties whose name matches the following regular expression must respect the following conditions

+ Property name regular expression: ^[a-z][a-z0-9]*(_[a-z0-9]+)*$ +
+ +

Point Pointset Model

Type: object
+

Information about a specific point name of the device.

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Expected unit configuration for the point

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Actual input unit that is sent from the device

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: enum (of string)
+

Expected data type for the point

+
+

Must be one of:

+
  • "string"
  • "boolean"
  • "number"
+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Detailed description of this point

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: boolean
+

Indicates if this point is writable (else read-only)

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+

Represents the expected baseline value of the point

+
+ + + + + +
+
Example:
+
22
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: number
+

Maximum deviation from baseline_value

+
+ + + + + +
+
Example:
+
2
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: enum (of string)
+

Expected state when baseline_value is set as the set_value for this point the config message

+
+

Must be one of:

+
  • "applied"
  • "updating"
  • "overridden"
  • "invalid"
  • "failure"
+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: number
+

Represents the lower bound of the error threshold for a point

+
+ + + + + +
+
Example:
+
0
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: number
+

Represents the upper bound of the error threshold for a point

+
+ + + + + +
+
Example:
+
100
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: integer
+

Represents the limit in seconds that a point can be unchanged for

+
+ + + + + +
+
Example:
+
3600
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: number
+

Triggering threshold for partial cov update publishing

+
+ + + + + +
+
Example:
+
1
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Mapping for the point to an internal resource (e.g. BACnet object reference)

+
+ + + + + +
+
Examples:
+
"AI3"
+
+
"400070"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Complete resolved URL of this point as determined by the gateway and ref blocks

+
+ + + + + +
+
Example:
+
"bacnet://1234/AV/1"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+ No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+

+ +

+

All properties whose name matches the following regular expression must respect the following conditions

+ Property name regular expression: ^[a-z_]+$ +
+ + Type: string
+ + + + + + + +
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: array
+

Tags assosciated with the point

+
+ + + + + +

All items must be unique

No Additional Items

Each item of this array must be:

+
+
+ + + Type: object
+Must match regular expression: ^[a-z0-9]+$ + + + + + + +
+

+
Examples:
+
[
+    "lighting"
+]
+
+
[
+    "energy",
+    "hvac"
+]
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+

Collection of family point information

+
No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+

+ +

+

All properties whose name matches the following regular expression must respect the following conditions

+ Property name regular expression: ^[-_.:/a-zA-Z0-9]+$ +
+ +

Ref Discovery

Type: object
+

Object representation for for a single point reference discovery

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Point descriptor for this point

+
Must match regular expression: ^[a-z][a-z0-9]*(_[a-z0-9]+)*$ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Friendly name for the reference, if known

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: array of string
+

List of possible enumerated values for the point

+
+ + + + + + No Additional Items

Each item of this array must be:

+
+
+ + + Type: string
+ + + + + + + +
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Current or default unit for this point

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Family reference for structures

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Current or default type for this point

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: boolean
+

Indicates if this point is writable or not

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Human-readable description of this point

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ +

Entry

Type: object
+ + + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+

A human-readable one-line description of the entry

+
+ + + + + +
+
Example:
+
"Point is not writable"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

An optional extensive entry which can include more detail, e.g. a complete program stack-trace

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ +

Category

Type: string
+ + +

+ +

+
+ + + Type: object
+Must match regular expression: ^system\.base\.start$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^system\.base\.shutdown$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^system\.base\.ready$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^system\.config\.receive$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^system\.config\.parse$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^system\.config\.apply$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^system\.auth\.login$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^system\.auth\.logout$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^system\.auth\.fail$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^pointset\.point\.nominal$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^pointset\.point\.applied$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^pointset\.point\.updating$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^pointset\.point\.overridden$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^pointset\.point\.failure$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^pointset\.point\.invalid$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^localnet\.network$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^localnet\.network\.connect$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^gateway\.setup\.attach$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^gateway\.proxy\.target$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^gateway\.proxy\.connect$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^discovery\.family\.scan$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^discovery\.device\.enumerate$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^discovery\.point\.describe$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^mapping\.device\.apply$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^blobset\.blob$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^blobset\.blob\.update$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^blobset\.blob\.receive$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^blobset\.blob\.fetch$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^blobset\.blob\.parse$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^blobset\.blob\.apply$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^blobset\.blob\.abort$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^blobset\.blob\.rollback$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^validation\.device\.receive$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^validation\.device\.schema$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^validation\.device\.content$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^validation\.device\.multiple$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^validation\.device\.extra$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^validation\.summary\.report$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^validation\.feature\.sequence$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^validation\.feature\.schema$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^validation\.feature\.capability$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^device(\.[a-z]+((_[a-z]+))*)+$ + + + + + + +
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: stringFormat: date-time
+

FC 3339 UTC timestamp the condition was triggered, or most recently updated

+
+ + + + + +
+
Example:
+
"2018-08-26T21:39:28.364Z"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: integer
+

The status level should conform to the numerical Stackdriver LogEntry levels. The DEFAULT value of 0 is not allowed (lowest value is 100, maximum 800).

+
+ + + +

Value must be greater or equal to 100 and lesser or equal to 800 and a multiple of 1

+ +
+
Example:
+
600
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ +

Ancillary Properties

Type: object
+

Arbitrary blob of json associated with this point

+
+ + + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+ No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+

+ +

+

All properties whose name matches the following regular expression must respect the following conditions

+ Property name regular expression: ^[a-z_]+$ +
+ + Type: string
+ + + + + + + +
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+ + + + + + + +
+
+
+

+ +

+
+ +
+
+ +

Family Discovery

Type: object
+

Discovery information for a protocol family.

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Device addr in the namespace of the given family

+
Must match regular expression: ^[-_.:0-9A-Za-z]+$ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: integer
+

Port number for the family connection (e.g. UDP port for BACnet/IP)

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Point reference in the namespace of the given family

+
Must match regular expression: ^[-_.:/#0-9A-Za-z]+$ + + + + + + +
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+

Reference links to alternate families

+
No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+

+ +

+

All properties whose name matches the following regular expression must respect the following conditions

+ Property name regular expression: ^[a-z][a-z0-9]*(_[a-z0-9]+)*$ +
+ +

Family Discovery

Type: object
+

Discovery information for a protocol family.

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Device addr in the namespace of the given family

+
Must match regular expression: ^[-_.:0-9A-Za-z]+$ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: integer
+

Port number for the family connection (e.g. UDP port for BACnet/IP)

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Point reference in the namespace of the given family

+
Must match regular expression: ^[-_.:/#0-9A-Za-z]+$ + + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Virtual equipment mapping linking this local point to a remote point in another device

+
Must match regular expression: ^[-0-9a-zA-Z$]+:[a-z0-9_]+$ + + + + + +
+
Example:
+
"VAV-3:supply_air_flowrate_sensor"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+

State value mapping for this point (e.g. 0.0: OFF)

+
+ + + + + + +
+
+
+

+ +

+
+ +
+

Each additional property must conform to the following schema

+ + Type: string
+ + + + + + + +
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ +

Building Translation

Type: object
+

Discovery result with implicit enumeration

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+

dotted path to present_value field

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+ + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+ + + + + + + +
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Actual input unit that is sent from the device

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+ + + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+

Collection of point references

+
No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+

+ +

+

All properties whose name matches the following regular expression must respect the following conditions

+ Property name regular expression: ^[-_.:/#a-zA-Z0-9]+$ +
+ +

Ref Discovery

Type: object
+

Object representation for for a single point reference discovery

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Point descriptor for this point

+
Must match regular expression: ^[a-z][a-z0-9]*(_[a-z0-9]+)*$ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Friendly name for the reference, if known

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: array of string
+

List of possible enumerated values for the point

+
+ + + + + + No Additional Items

Each item of this array must be:

+
+
+ + + Type: string
+ + + + + + + +
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Current or default unit for this point

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Family reference for structures

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Current or default type for this point

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: boolean
+

Indicates if this point is writable or not

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Human-readable description of this point

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ +

Entry

Type: object
+ + + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+

A human-readable one-line description of the entry

+
+ + + + + +
+
Example:
+
"Point is not writable"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

An optional extensive entry which can include more detail, e.g. a complete program stack-trace

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ +

Category

Type: string
+ + +

+ +

+
+ + + Type: object
+Must match regular expression: ^system\.base\.start$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^system\.base\.shutdown$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^system\.base\.ready$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^system\.config\.receive$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^system\.config\.parse$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^system\.config\.apply$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^system\.auth\.login$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^system\.auth\.logout$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^system\.auth\.fail$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^pointset\.point\.nominal$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^pointset\.point\.applied$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^pointset\.point\.updating$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^pointset\.point\.overridden$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^pointset\.point\.failure$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^pointset\.point\.invalid$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^localnet\.network$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^localnet\.network\.connect$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^gateway\.setup\.attach$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^gateway\.proxy\.target$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^gateway\.proxy\.connect$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^discovery\.family\.scan$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^discovery\.device\.enumerate$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^discovery\.point\.describe$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^mapping\.device\.apply$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^blobset\.blob$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^blobset\.blob\.update$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^blobset\.blob\.receive$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^blobset\.blob\.fetch$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^blobset\.blob\.parse$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^blobset\.blob\.apply$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^blobset\.blob\.abort$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^blobset\.blob\.rollback$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^validation\.device\.receive$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^validation\.device\.schema$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^validation\.device\.content$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^validation\.device\.multiple$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^validation\.device\.extra$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^validation\.summary\.report$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^validation\.feature\.sequence$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^validation\.feature\.schema$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^validation\.feature\.capability$ + + + + + + +
+ + + Type: object
+Must match regular expression: ^device(\.[a-z]+((_[a-z]+))*)+$ + + + + + + +
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: stringFormat: date-time
+

FC 3339 UTC timestamp the condition was triggered, or most recently updated

+
+ + + + + +
+
Example:
+
"2018-08-26T21:39:28.364Z"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: integer
+

The status level should conform to the numerical Stackdriver LogEntry levels. The DEFAULT value of 0 is not allowed (lowest value is 100, maximum 800).

+
+ + + +

Value must be greater or equal to 100 and lesser or equal to 800 and a multiple of 1

+ +
+
Example:
+
600
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ +

Ancillary Properties

Type: object
+

Arbitrary blob of json associated with this point

+
+ + + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+ No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+

+ +

+

All properties whose name matches the following regular expression must respect the following conditions

+ Property name regular expression: ^[a-z_]+$ +
+ + Type: string
+ + + + + + + +
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+ + + + + + + +
+
+
+

+ +

+
+ +
+
+ +

Family Discovery

Type: object
+

Discovery information for a protocol family.

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Device addr in the namespace of the given family

+
Must match regular expression: ^[-_.:0-9A-Za-z]+$ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: integer
+

Port number for the family connection (e.g. UDP port for BACnet/IP)

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Point reference in the namespace of the given family

+
Must match regular expression: ^[-_.:/#0-9A-Za-z]+$ + + + + + + +
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+

Reference links to alternate families

+
No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+

+ +

+

All properties whose name matches the following regular expression must respect the following conditions

+ Property name regular expression: ^[a-z][a-z0-9]*(_[a-z0-9]+)*$ +
+ +

Family Discovery

Type: object
+

Discovery information for a protocol family.

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Device addr in the namespace of the given family

+
Must match regular expression: ^[-_.:0-9A-Za-z]+$ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: integer
+

Port number for the family connection (e.g. UDP port for BACnet/IP)

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Point reference in the namespace of the given family

+
Must match regular expression: ^[-_.:/#0-9A-Za-z]+$ + + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+

Discovery of features supported by this device.

+
No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+

+ +

+

All properties whose name matches the following regular expression must respect the following conditions

+ Property name regular expression: ^[._a-zA-Z]+$ +
+ +

Feature Discovery

Type: object
+

Object representation for for a single feature

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ +

FeatureStage

Type: enum (of string)
+

Stage of a feature implemenation

+
+ +
+

Must be one of:

+
  • "disabled"
  • "alpha"
  • "preview"
  • "beta"
  • "stable"
+
+ + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ +

Cloud Model

Type: object
+

Information specific to how the device communicates with the cloud.

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: integer
+

Function version of this request

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: enum (of string)
+

The key type used for cloud communication.

+
+

Must be one of:

+
  • "ES256"
  • "ES256_X509"
  • "RS256"
  • "RS256_X509"
+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: boolean
+

Indicates if the device private key is provided in the site model.

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: enum (of string)
+

The cloud resource type

+
+

Must be one of:

+
  • "DIRECT"
  • "GATEWAY"
  • "FUNNEL"
  • "PROXIED"
  • "REGISTRY"
+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ +

Gateway Model

Type: object
+

Gateway Documentation

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+

The device ID of the gateway the device is bound to

+
Must match regular expression: ^[A-Z]{2,6}-[1-9][0-9]*$ + + + + + +
+
Example:
+
"GAT-100"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ +

Family Localnet Model

Type: object
+

The type of network

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+

The address of a device on the fieldbus/local network

+
+ + + + + +
+
Examples:
+
"192.168.1.3"
+
+
"41489"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

The slave unit ID of the device for protocol families like modbus

+
+ + + + + +
+
Examples:
+
"1"
+
+
"255"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

The network designator for this device in the family-defined format

+
+ + + + + +
+
Examples:
+
"192.168.1.0/24"
+
+
"12732"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+ No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+

+ +

+

All properties whose name matches the following regular expression must respect the following conditions

+ Property name regular expression: ^[a-z_]+$ +
+ + Type: string
+ + + + + + + +
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Specifies that this is a shadow of the indicated device

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

The device id of the node's parent

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: boolean
+

If true, then the system should not try to validate the refs

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

The family designator, used only when the entry is not keyed in a family map

+
+ + + + + +
+
Examples:
+
"ipv4"
+
+
"bacnet"
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: array of string
+

An array of all the device IDs which are bound to the device

+
+ + + + + + No Additional Items

Each item of this array must be:

+
+
+ + + Type: string
+Must match regular expression: ^[A-Z]{2,6}-[1-9][0-9]*$ + + + + + + +
+

+
Example:
+
[
+    "AHU-22"
+]
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ +

Config Cloud Model

Type: object
+

Information specific to how the device communicates with the cloud.

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Config file to use. Within the config directory in the device's metadata directory

+
+ + + + + + +
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: boolean
+ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: stringFormat: date-time
+ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: stringFormat: date-time
+ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: stringFormat: date-time
+ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: stringFormat: date-time
+ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+ + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ +

Model Operation

Type: enum (of string)
+ + +
+

Must be one of:

+
  • "ERROR"
  • "CREATE"
  • "READ"
  • "REPLY"
  • "UPDATE"
  • "DELETE"
  • "MODIFY"
  • "PREVIEW"
  • "BIND"
  • "UNBIND"
  • "BOUND"
  • "ALLOW"
  • "BLOCK"
+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Strigified version of the metadata object, used for internal backend processing

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: object
+

If operating on the entire registry, then this contains results for multiple devices (batched operation).

+
+ + + + + + +
+
+
+

+ +

+
+ +
+

+ +

+

All properties whose name matches the following regular expression must respect the following conditions

+ Property name regular expression: ^[-._a-zA-Z0-9]+$ +
+ + Type: object
+ + + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ +

System Discovery Data

Type: object
+ No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Full textual desctiiption of this device

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Friendly name of this device

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

The serial number of the physical device

+
+ + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ +

Ancillary Properties

Type: object
+

Arbitrary blob of json associated with this point

+
+ + + + + + + + +
+
+
+
+
+
+
+

+ +

+
+ +
+
+ +

State System Hardware

Type: object
+

A collection of fields which describe the physical hardware of the device.

+
+ + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+

The manufacturer of the device

+
+ + + + + +
+
Example:
+
"Google"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

The model of the device

+
+ + + + + +
+
Examples:
+
"Pixel 5"
+
+
"Pixel 6"
+
+
"Pixel 6 Pro"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

A stock keeping unit which identifies the unique composition of a device, for example those with different hardware configurations

+
+ + + + + +
+
Examples:
+
"GF5KQ"
+
+
"4215612"
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Hardware revision of the device

+
+ + + + + +
+
Examples:
+
"A"
+
+
"B"
+
+
"1"
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ +

Metadata

Type: object
+

Device metadata model (for model discovery)

+
Same definition as model +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ +

+
+ +
+
+ +

Policy Model

Type: object
+ + + No Additional Properties + + + + + + +
+
+
+

+ +

+
+ +
+
+ + Type: array of string
+

An array of rule sets which are applicable to the device

+
+ + + + + + No Additional Items

Each item of this array must be:

+
+
+ + + Type: string
+Must match regular expression: ^[a-z_0-9]*$ + + + + + + +
+

+
Example:
+
[
+    "ip",
+    "mqtt"
+]
+
diff --git a/gencode/docs/metadata.html b/gencode/docs/metadata.html index 3d7650f58f..137e32b2eb 100644 --- a/gencode/docs/metadata.html +++ b/gencode/docs/metadata.html @@ -25026,6 +25026,54 @@

+

+
+
+
+
+
+
+

+ +

+
+ +
+
+ + Type: integer
+

Total number of records expected for this active scan generation

+
+ + + +

Value must be greater or equal to 0

+ +
@@ -45874,6 +45922,48 @@

+
+
+
+
+

+ +

+
+ +
+
+ +

Metadata

Type: object
+

Device metadata model (for model discovery)

+
Same definition as Metadata +
+
+
diff --git a/gencode/java/udmi/schema/DiscoveryEvents.java b/gencode/java/udmi/schema/DiscoveryEvents.java index 9839f93ef8..48148bac41 100644 --- a/gencode/java/udmi/schema/DiscoveryEvents.java +++ b/gencode/java/udmi/schema/DiscoveryEvents.java @@ -70,6 +70,13 @@ public class DiscoveryEvents { @JsonProperty("event_no") @JsonPropertyDescription("The active or passive series number of this result (matches reported state values)") public Integer event_no; + /** + * Total number of records expected for this active scan generation + * + */ + @JsonProperty("active_count") + @JsonPropertyDescription("Total number of records expected for this active scan generation") + public Integer active_count; /** * Links to other address families (family and id) * @@ -129,12 +136,22 @@ public class DiscoveryEvents { */ @JsonProperty("system") public SystemDiscoveryData system; + /** + * Metadata + *

+ * [Metadata](../docs/specs/metadata.md) is a description about the device: a specification about how the device should be configured and expectations about what the device should be doing. Defined by `metadata.json` + * + */ + @JsonProperty("model") + @JsonPropertyDescription("[Metadata](../docs/specs/metadata.md) is a description about the device: a specification about how the device should be configured and expectations about what the device should be doing. Defined by `metadata.json`") + public Metadata model; @Override public int hashCode() { int result = 1; result = ((result* 31)+((this.generation == null)? 0 :this.generation.hashCode())); result = ((result* 31)+((this.devices == null)? 0 :this.devices.hashCode())); + result = ((result* 31)+((this.active_count == null)? 0 :this.active_count.hashCode())); result = ((result* 31)+((this.registries == null)? 0 :this.registries.hashCode())); result = ((result* 31)+((this.families == null)? 0 :this.families.hashCode())); result = ((result* 31)+((this.version == null)? 0 :this.version.hashCode())); @@ -144,6 +161,7 @@ public int hashCode() { result = ((result* 31)+((this.system == null)? 0 :this.system.hashCode())); result = ((result* 31)+((this.event_no == null)? 0 :this.event_no.hashCode())); result = ((result* 31)+((this.refs == null)? 0 :this.refs.hashCode())); + result = ((result* 31)+((this.model == null)? 0 :this.model.hashCode())); result = ((result* 31)+((this.family == null)? 0 :this.family.hashCode())); result = ((result* 31)+((this.addr == null)? 0 :this.addr.hashCode())); result = ((result* 31)+((this.cloud_model == null)? 0 :this.cloud_model.hashCode())); @@ -161,7 +179,7 @@ public boolean equals(Object other) { return false; } DiscoveryEvents rhs = ((DiscoveryEvents) other); - return (((((((((((((((((this.generation == rhs.generation)||((this.generation!= null)&&this.generation.equals(rhs.generation)))&&((this.devices == rhs.devices)||((this.devices!= null)&&this.devices.equals(rhs.devices))))&&((this.registries == rhs.registries)||((this.registries!= null)&&this.registries.equals(rhs.registries))))&&((this.families == rhs.families)||((this.families!= null)&&this.families.equals(rhs.families))))&&((this.version == rhs.version)||((this.version!= null)&&this.version.equals(rhs.version))))&&((this.network == rhs.network)||((this.network!= null)&&this.network.equals(rhs.network))))&&((this.points == rhs.points)||((this.points!= null)&&this.points.equals(rhs.points))))&&((this.features == rhs.features)||((this.features!= null)&&this.features.equals(rhs.features))))&&((this.system == rhs.system)||((this.system!= null)&&this.system.equals(rhs.system))))&&((this.event_no == rhs.event_no)||((this.event_no!= null)&&this.event_no.equals(rhs.event_no))))&&((this.refs == rhs.refs)||((this.refs!= null)&&this.refs.equals(rhs.refs))))&&((this.family == rhs.family)||((this.family!= null)&&this.family.equals(rhs.family))))&&((this.addr == rhs.addr)||((this.addr!= null)&&this.addr.equals(rhs.addr))))&&((this.cloud_model == rhs.cloud_model)||((this.cloud_model!= null)&&this.cloud_model.equals(rhs.cloud_model))))&&((this.timestamp == rhs.timestamp)||((this.timestamp!= null)&&this.timestamp.equals(rhs.timestamp))))&&((this.status == rhs.status)||((this.status!= null)&&this.status.equals(rhs.status)))); + return (((((((((((((((((((this.generation == rhs.generation)||((this.generation!= null)&&this.generation.equals(rhs.generation)))&&((this.devices == rhs.devices)||((this.devices!= null)&&this.devices.equals(rhs.devices))))&&((this.active_count == rhs.active_count)||((this.active_count!= null)&&this.active_count.equals(rhs.active_count))))&&((this.registries == rhs.registries)||((this.registries!= null)&&this.registries.equals(rhs.registries))))&&((this.families == rhs.families)||((this.families!= null)&&this.families.equals(rhs.families))))&&((this.version == rhs.version)||((this.version!= null)&&this.version.equals(rhs.version))))&&((this.network == rhs.network)||((this.network!= null)&&this.network.equals(rhs.network))))&&((this.points == rhs.points)||((this.points!= null)&&this.points.equals(rhs.points))))&&((this.features == rhs.features)||((this.features!= null)&&this.features.equals(rhs.features))))&&((this.system == rhs.system)||((this.system!= null)&&this.system.equals(rhs.system))))&&((this.event_no == rhs.event_no)||((this.event_no!= null)&&this.event_no.equals(rhs.event_no))))&&((this.refs == rhs.refs)||((this.refs!= null)&&this.refs.equals(rhs.refs))))&&((this.model == rhs.model)||((this.model!= null)&&this.model.equals(rhs.model))))&&((this.family == rhs.family)||((this.family!= null)&&this.family.equals(rhs.family))))&&((this.addr == rhs.addr)||((this.addr!= null)&&this.addr.equals(rhs.addr))))&&((this.cloud_model == rhs.cloud_model)||((this.cloud_model!= null)&&this.cloud_model.equals(rhs.cloud_model))))&&((this.timestamp == rhs.timestamp)||((this.timestamp!= null)&&this.timestamp.equals(rhs.timestamp))))&&((this.status == rhs.status)||((this.status!= null)&&this.status.equals(rhs.status)))); } } diff --git a/gencode/java/udmi/schema/FamilyDiscoveryConfig.java b/gencode/java/udmi/schema/FamilyDiscoveryConfig.java index d74ddf2990..990c2fbd31 100644 --- a/gencode/java/udmi/schema/FamilyDiscoveryConfig.java +++ b/gencode/java/udmi/schema/FamilyDiscoveryConfig.java @@ -52,6 +52,13 @@ public class FamilyDiscoveryConfig { public Integer passive_sec; @JsonProperty("depth") public udmi.schema.Enumerations.Depth depth; + /** + * Optional source qualifier for the discovery scan (e.g., 'model') + * + */ + @JsonProperty("source") + @JsonPropertyDescription("Optional source qualifier for the discovery scan (e.g., 'model')") + public String source; /** * Trace Discovery Config *

@@ -72,6 +79,7 @@ public int hashCode() { result = ((result* 31)+((this.passive_sec == null)? 0 :this.passive_sec.hashCode())); result = ((result* 31)+((this.scan_duration_sec == null)? 0 :this.scan_duration_sec.hashCode())); result = ((result* 31)+((this.addrs == null)? 0 :this.addrs.hashCode())); + result = ((result* 31)+((this.source == null)? 0 :this.source.hashCode())); result = ((result* 31)+((this.networks == null)? 0 :this.networks.hashCode())); return result; } @@ -85,7 +93,7 @@ public boolean equals(Object other) { return false; } FamilyDiscoveryConfig rhs = ((FamilyDiscoveryConfig) other); - return (((((((((this.generation == rhs.generation)||((this.generation!= null)&&this.generation.equals(rhs.generation)))&&((this.scan_interval_sec == rhs.scan_interval_sec)||((this.scan_interval_sec!= null)&&this.scan_interval_sec.equals(rhs.scan_interval_sec))))&&((this.trace == rhs.trace)||((this.trace!= null)&&this.trace.equals(rhs.trace))))&&((this.depth == rhs.depth)||((this.depth!= null)&&this.depth.equals(rhs.depth))))&&((this.passive_sec == rhs.passive_sec)||((this.passive_sec!= null)&&this.passive_sec.equals(rhs.passive_sec))))&&((this.scan_duration_sec == rhs.scan_duration_sec)||((this.scan_duration_sec!= null)&&this.scan_duration_sec.equals(rhs.scan_duration_sec))))&&((this.addrs == rhs.addrs)||((this.addrs!= null)&&this.addrs.equals(rhs.addrs))))&&((this.networks == rhs.networks)||((this.networks!= null)&&this.networks.equals(rhs.networks)))); + return ((((((((((this.generation == rhs.generation)||((this.generation!= null)&&this.generation.equals(rhs.generation)))&&((this.scan_interval_sec == rhs.scan_interval_sec)||((this.scan_interval_sec!= null)&&this.scan_interval_sec.equals(rhs.scan_interval_sec))))&&((this.trace == rhs.trace)||((this.trace!= null)&&this.trace.equals(rhs.trace))))&&((this.depth == rhs.depth)||((this.depth!= null)&&this.depth.equals(rhs.depth))))&&((this.passive_sec == rhs.passive_sec)||((this.passive_sec!= null)&&this.passive_sec.equals(rhs.passive_sec))))&&((this.scan_duration_sec == rhs.scan_duration_sec)||((this.scan_duration_sec!= null)&&this.scan_duration_sec.equals(rhs.scan_duration_sec))))&&((this.addrs == rhs.addrs)||((this.addrs!= null)&&this.addrs.equals(rhs.addrs))))&&((this.source == rhs.source)||((this.source!= null)&&this.source.equals(rhs.source))))&&((this.networks == rhs.networks)||((this.networks!= null)&&this.networks.equals(rhs.networks)))); } } diff --git a/gencode/python/udmi/schema/config_discovery_family.py b/gencode/python/udmi/schema/config_discovery_family.py index df10c41dcf..c7cd0b640a 100644 --- a/gencode/python/udmi/schema/config_discovery_family.py +++ b/gencode/python/udmi/schema/config_discovery_family.py @@ -24,4 +24,5 @@ class FamilyDiscoveryConfig(DataModel): networks: Optional[List[str]] = None passive_sec: Optional[int] = None depth: Optional[Depth] = None + source: Optional[str] = None trace: Optional[TraceDiscoveryConfig] = None diff --git a/gencode/python/udmi/schema/events_discovery.py b/gencode/python/udmi/schema/events_discovery.py index 6fe9294118..f96c707683 100644 --- a/gencode/python/udmi/schema/events_discovery.py +++ b/gencode/python/udmi/schema/events_discovery.py @@ -40,6 +40,7 @@ class DiscoveryEvents(DataModel): addr: Optional[str] = None network: Optional[str] = None event_no: Optional[int] = None + active_count: Optional[int] = None families: Optional[Dict[str, FamilyDiscovery]] = None registries: Optional[Dict[str, CloudModel]] = None devices: Optional[Dict[str, CloudModel]] = None @@ -48,3 +49,6 @@ class DiscoveryEvents(DataModel): features: Optional[Dict[str, FeatureDiscovery]] = None cloud_model: Optional[CloudModel] = None system: Optional[System] = None + model: Optional[Metadata] = None + +from .metadata import Metadata diff --git a/gencode/python/udmi/schema/metadata.py b/gencode/python/udmi/schema/metadata.py index 1f8ac5e28a..38e1e6af58 100644 --- a/gencode/python/udmi/schema/metadata.py +++ b/gencode/python/udmi/schema/metadata.py @@ -8,7 +8,6 @@ from ._base import DataModel from .common import Operation -from .events_discovery import DiscoveryEvents from .model_alarmset import AlarmsetModel from .model_cloud import CloudModel from .model_discovery import DiscoveryModel @@ -47,3 +46,5 @@ class Metadata(DataModel): pointset: Optional[PointsetModel] = None structure: Optional[Dict[str, DiscoveryEvents]] = None policy: Optional[PolicyModel] = None + +from .events_discovery import DiscoveryEvents diff --git a/mcp/butler/client.py b/mcp/butler/client.py index a70f88c51f..228ef2da9b 100644 --- a/mcp/butler/client.py +++ b/mcp/butler/client.py @@ -93,6 +93,10 @@ def get_discovered_devices(self, registry_id: str) -> List[Dict[str, Any]]: """Fetch normalized discovered devices with addresses for reconciliation mapping.""" return self.call_rpc("get_discovered_devices", {"registry_id": registry_id}) + def get_discovered_models(self, registry_id: str) -> Dict[str, Dict[str, Any]]: + """Fetch discovered device models (Metadata) for a registry from Model Discovery events.""" + return self.call_rpc("get_discovered_models", {"registry_id": registry_id}) + def get_device_messages(self, registry_id: str, device_id: str) -> List[Dict[str, Any]]: """Fetch chronological lifecycle messages (models, discovery, proposals) for a device.""" return self.call_rpc("get_device_messages", {"registry_id": registry_id, "device_id": device_id}) diff --git a/mcp/butler/provider.py b/mcp/butler/provider.py index da364a7818..8c5f0c8f12 100644 --- a/mcp/butler/provider.py +++ b/mcp/butler/provider.py @@ -256,6 +256,9 @@ def get_discovered_devices(self, registry_id: str) -> List[Dict[str, Any]]: if isinstance(payload, dict) and "payload" in payload and isinstance(payload.get("payload"), dict): payload = payload["payload"] + if not isinstance(payload, dict) or payload.get("family") == "iot": + continue + bacnet_addr = None ipv4_addr = None vendor_addr = None @@ -288,6 +291,82 @@ def get_discovered_devices(self, registry_id: str) -> List[Dict[str, Any]]: return devices + def get_discovered_models(self, registry_id: str) -> Dict[str, Dict[str, Any]]: + """Retrieves discovered device models (Metadata) for a registry from Model Discovery events or metadata store. + + Args: + registry_id: Target device registry identifier. + + Returns: + Dictionary mapping device_id to its latest Metadata dictionary. + """ + models: Dict[str, Dict[str, Any]] = {} + if not self.pg_manager: + return models + + try: + conn = self.pg_manager.get_connection() + with conn.cursor() as cur: + cur.execute( + """ + SELECT DISTINCT ON (device_id) device_id, metadata + FROM udmi_metadata + WHERE device_registry_id = %s AND metadata IS NOT NULL + ORDER BY device_id, timestamp DESC; + """, + (registry_id,), + ) + meta_rows = cur.fetchall() + for dev_id, meta_payload in meta_rows: + if isinstance(meta_payload, str): + try: + meta_payload = json.loads(meta_payload) + except Exception: + continue + if dev_id and isinstance(meta_payload, dict): + models[str(dev_id)] = meta_payload + + cur.execute( + """ + SELECT device_id, sub_type, sub_folder, payload + FROM udmi_messages + WHERE registry_id = %s + AND ( + (sub_folder = 'discovery' AND sub_type = 'events') + OR sub_type = 'model' + ) + ORDER BY id ASC; + """, + (registry_id,), + ) + msg_rows = cur.fetchall() + conn.close() + + for dev_id, sub_type, sub_folder, payload in msg_rows: + if isinstance(payload, str): + try: + payload = json.loads(payload) + except Exception: + continue + if isinstance(payload, dict) and "payload" in payload and isinstance(payload.get("payload"), dict): + payload = payload["payload"] + if not isinstance(payload, dict): + continue + + if sub_folder == "discovery" and sub_type == "events": + if payload.get("family") == "iot" and isinstance(payload.get("model"), dict): + target_dev = payload.get("addr") or dev_id + if target_dev: + models[str(target_dev)] = payload["model"] + elif sub_type == "model": + if dev_id: + models[str(dev_id)] = payload + + return models + except Exception as e: + print(f"ButlerProvider error fetching discovered models: {e}", file=sys.stderr) + return models + def get_device_messages( self, registry_id: str, @@ -562,6 +641,11 @@ def clear_registry_mapping_data(self, registry_id: str) -> Dict[str, Any]: (registry_id,), ) deleted_count += cur.rowcount + cur.execute( + "DELETE FROM udmi_metadata WHERE device_registry_id = %s;", + (registry_id,), + ) + deleted_count += cur.rowcount conn.commit() finally: diff --git a/mcp/butler/server.py b/mcp/butler/server.py index 3e04c733cd..f7e2ff57a0 100644 --- a/mcp/butler/server.py +++ b/mcp/butler/server.py @@ -59,6 +59,22 @@ "required": ["registry_id"], }, }, + { + "name": "get_discovered_models", + "description": ( + "Retrieve discovered device models (Metadata) for a registry from Model Discovery events." + ), + "inputSchema": { + "type": "object", + "properties": { + "registry_id": { + "type": "string", + "description": "The target device registry identifier", + } + }, + "required": ["registry_id"], + }, + }, { "name": "get_device_messages", "description": ( @@ -439,6 +455,8 @@ def _dispatch_tool(self, name: str, args: Dict[str, Any]) -> Any: return self.provider.get_discovery_events(registry_id=args["registry_id"]) if name == "get_discovered_devices": return self.provider.get_discovered_devices(registry_id=args["registry_id"]) + if name == "get_discovered_models": + return self.provider.get_discovered_models(registry_id=args["registry_id"]) if name == "get_device_messages": return self.provider.get_device_messages( registry_id=args["registry_id"], device_id=args["device_id"] @@ -656,6 +674,10 @@ def main() -> None: devs_parser.add_argument("registry_id", help="Registry ID") devs_parser.add_argument("--pg-port", type=int, default=None) + models_parser = subparsers.add_parser("discovered-models", help="List discovered models for registry") + models_parser.add_argument("registry_id", help="Registry ID") + models_parser.add_argument("--pg-port", type=int, default=None) + msgs_parser = subparsers.add_parser("device-messages", help="List lifecycle messages for a device") msgs_parser.add_argument("registry_id", help="Registry ID") msgs_parser.add_argument("device_id", help="Device ID") @@ -694,6 +716,11 @@ def main() -> None: print(json.dumps(devs, indent=2)) return + if args.command == "discovered-models": + models = provider.get_discovered_models(args.registry_id) + print(json.dumps(models, indent=2)) + return + if args.command == "device-messages": msgs = provider.get_device_messages(args.registry_id, args.device_id) print(json.dumps(msgs, indent=2)) diff --git a/mcp/uufi/provider.py b/mcp/uufi/provider.py index acc68835b6..a9c858d752 100644 --- a/mcp/uufi/provider.py +++ b/mcp/uufi/provider.py @@ -105,6 +105,8 @@ def get_topic( prefix_seg = f"/{self.prefix}" if self.prefix else "" if registry_id and device_id: return f"{prefix_seg}/uufi/r/{registry_id}/d/{device_id}/c/{sub_type}/{sub_folder}" + if registry_id: + return f"{prefix_seg}/uufi/r/{registry_id}/c/{sub_type}/{sub_folder}" return f"{prefix_seg}/uufi/c/{sub_type}/{sub_folder}" def create_envelope( diff --git a/schema/category.json b/schema/category.json index d6ea72c022..54d9a76d53 100644 --- a/schema/category.json +++ b/schema/category.json @@ -1,7 +1,6 @@ { "$udmi_version": "1.5.7", "title": "Category", - "$schema": "http://json-schema.org/draft-07/schema#", "$comment": "Auto-generated category mappings from bin/gencode_categories.", "type": "string", "oneOf": [ diff --git a/schema/config_discovery_family.json b/schema/config_discovery_family.json index d8e3304eda..61423b9a75 100644 --- a/schema/config_discovery_family.json +++ b/schema/config_discovery_family.json @@ -44,6 +44,10 @@ "description": "Indicates implicit enumeration of discovered devices", "$ref": "file:common.json#/definitions/depth" }, + "source": { + "description": "Optional source qualifier for the discovery scan (e.g., 'model')", + "type": "string" + }, "trace": { "description": "Trace discovery parameters (when depth is 'trace')", "$ref": "file:config_discovery_trace.json" diff --git a/schema/events_discovery.json b/schema/events_discovery.json index 50d432083d..63ee409bca 100644 --- a/schema/events_discovery.json +++ b/schema/events_discovery.json @@ -41,6 +41,11 @@ "description": "The active or passive series number of this result (matches reported state values)", "type": "integer" }, + "active_count": { + "description": "Total number of records expected for this active scan generation", + "minimum": 0, + "type": "integer" + }, "families": { "description": "Links to other address families (family and id)", "type": "object", @@ -131,6 +136,10 @@ "$ref": "file:state_system_hardware.json" } } + }, + "model": { + "description": "Device metadata model (for model discovery)", + "$ref": "file:metadata.json" } } } diff --git a/tests/schemas/config/model_discovery.json b/tests/schemas/config/model_discovery.json new file mode 100644 index 0000000000..d70ab9e666 --- /dev/null +++ b/tests/schemas/config/model_discovery.json @@ -0,0 +1,13 @@ +{ + "version": "1.5.7", + "timestamp": "2026-04-06T20:10:00Z", + "discovery": { + "families": { + "iot": { + "generation": "2026-04-06T20:10:00Z", + "depth": "details", + "source": "model" + } + } + } +} diff --git a/tests/schemas/config/model_discovery.out b/tests/schemas/config/model_discovery.out new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/schemas/events_discovery/model_discovery_device.json b/tests/schemas/events_discovery/model_discovery_device.json new file mode 100644 index 0000000000..071e4e14ce --- /dev/null +++ b/tests/schemas/events_discovery/model_discovery_device.json @@ -0,0 +1,33 @@ +{ + "version": "1.5.7", + "timestamp": "2026-04-06T20:10:01Z", + "generation": "2026-04-06T20:10:00Z", + "event_no": 1, + "family": "iot", + "addr": "AHU-1", + "model": { + "version": "1.5.7", + "timestamp": "2026-04-06T20:00:00Z", + "system": { + "location": { + "site": "US-SFO-X1", + "section": "1F" + }, + "physical_tag": { + "asset": { + "guid": "drw://1234", + "site": "US-SFO-X1", + "name": "AHU-1" + } + } + }, + "pointset": { + "points": { + "filter_alarm_pressure_status": { + "units": "No-units", + "ref": "BV11.present_value" + } + } + } + } +} diff --git a/tests/schemas/events_discovery/model_discovery_device.out b/tests/schemas/events_discovery/model_discovery_device.out new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/schemas/events_discovery/model_discovery_start.json b/tests/schemas/events_discovery/model_discovery_start.json new file mode 100644 index 0000000000..e1ea2b09ad --- /dev/null +++ b/tests/schemas/events_discovery/model_discovery_start.json @@ -0,0 +1,8 @@ +{ + "version": "1.5.7", + "timestamp": "2026-04-06T20:10:01Z", + "generation": "2026-04-06T20:10:00Z", + "event_no": 0, + "family": "iot", + "active_count": 3 +} diff --git a/tests/schemas/events_discovery/model_discovery_start.out b/tests/schemas/events_discovery/model_discovery_start.out new file mode 100644 index 0000000000..e69de29bb2 diff --git a/udmis/src/main/java/com/google/bos/udmi/service/core/ControlProcessor.java b/udmis/src/main/java/com/google/bos/udmi/service/core/ControlProcessor.java index 96cd7ca001..79abf0310a 100644 --- a/udmis/src/main/java/com/google/bos/udmi/service/core/ControlProcessor.java +++ b/udmis/src/main/java/com/google/bos/udmi/service/core/ControlProcessor.java @@ -8,6 +8,7 @@ import java.util.Map.Entry; import java.util.Set; import udmi.schema.CloudQuery; +import udmi.schema.DiscoveryConfig; import udmi.schema.EndpointConfiguration; import udmi.schema.Envelope; import udmi.schema.Envelope.SubFolder; @@ -40,6 +41,15 @@ public void cloudQueryHandler(CloudQuery query) { CloudQueryHandler.processQuery(this, query); } + /** + * Handle a discovery config command. + */ + @MessageHandler + public void discoveryConfigHandler(DiscoveryConfig config) { + Envelope envelope = getContinuation(config).getEnvelope(); + ModelDiscoveryHandler.processDiscoveryConfig(this, envelope, config); + } + /** * Handle a udmi config command. */ diff --git a/udmis/src/main/java/com/google/bos/udmi/service/core/ModelDiscoveryHandler.java b/udmis/src/main/java/com/google/bos/udmi/service/core/ModelDiscoveryHandler.java new file mode 100644 index 0000000000..d03cbd5297 --- /dev/null +++ b/udmis/src/main/java/com/google/bos/udmi/service/core/ModelDiscoveryHandler.java @@ -0,0 +1,207 @@ +package com.google.bos.udmi.service.core; + +import static com.google.udmi.util.GeneralUtils.ifNotNullThen; +import static com.google.udmi.util.GeneralUtils.isTrue; +import static com.google.udmi.util.JsonUtil.fromString; +import static java.util.Optional.ofNullable; + +import com.google.bos.udmi.service.access.IotAccessBase; +import com.google.bos.udmi.service.pod.UdmiServicePod; +import com.google.udmi.util.MetadataMapKeys; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map.Entry; +import udmi.lib.ProtocolFamily; +import udmi.schema.CloudModel; +import udmi.schema.DiscoveryConfig; +import udmi.schema.DiscoveryEvents; +import udmi.schema.DiscoveryState; +import udmi.schema.Enumerations.Depth; +import udmi.schema.Envelope; +import udmi.schema.Envelope.SubFolder; +import udmi.schema.Envelope.SubType; +import udmi.schema.FamilyDiscoveryConfig; +import udmi.schema.FamilyDiscoveryState; +import udmi.schema.FamilyDiscoveryState.Phase; +import udmi.schema.Metadata; + +/** + * Handler for executing Site Model Discovery scans for a device registry. + */ +public class ModelDiscoveryHandler { + + public static final String MODEL_SOURCE = "model"; + + private final ProcessorBase processor; + private final IotAccessBase iotAccess; + private final String registryId; + private final FamilyDiscoveryConfig familyConfig; + private final Date generation; + + /** + * Create a new ModelDiscoveryHandler instance. + */ + public ModelDiscoveryHandler(ProcessorBase processor, String registryId, + FamilyDiscoveryConfig familyConfig, Date generation) { + this.processor = processor; + this.iotAccess = processor.iotAccess; + this.registryId = registryId; + this.familyConfig = familyConfig; + this.generation = generation; + } + + /** + * Determine whether the given envelope and discovery config target Site Model Discovery. + */ + public static boolean isModelDiscoveryConfig(Envelope envelope, DiscoveryConfig config) { + if (envelope == null || config == null + || envelope.subType != SubType.CONFIG || envelope.subFolder != SubFolder.DISCOVERY) { + return false; + } + FamilyDiscoveryConfig iotConfig = config.families != null + ? config.families.get(ProtocolFamily.IOT) + : null; + return iotConfig != null && MODEL_SOURCE.equals(iotConfig.source); + } + + /** + * Process a DiscoveryConfig targeting model discovery. + */ + public static void processDiscoveryConfig(ProcessorBase processor, Envelope envelope, + DiscoveryConfig config) { + if (!isModelDiscoveryConfig(envelope, config) || envelope.deviceRegistryId == null) { + return; + } + FamilyDiscoveryConfig iotConfig = config.families.get(ProtocolFamily.IOT); + processDiscovery(processor, envelope.deviceRegistryId, iotConfig, config.generation); + } + + /** + * Execute a model discovery scan for the given registry and family configuration. + */ + public static void processDiscovery(ProcessorBase processor, String registryId, + FamilyDiscoveryConfig familyConfig, Date fallbackGeneration) { + if (registryId == null) { + return; + } + Date generation = ofNullable(familyConfig).map(c -> c.generation).orElse(fallbackGeneration); + if (generation == null) { + return; + } + new ModelDiscoveryHandler(processor, registryId, familyConfig, generation).process(); + } + + /** + * Run the complete model discovery lifecycle: start state bookend, start/count event + * (event_no = 0), individual device model events (event_no = 1..N), and stop state bookend. + */ + public synchronized void process() { + processor.info("Starting model discovery for registry %s (generation %s)", + registryId, generation); + + publishState(Phase.ACTIVE, 0); + + CloudModel registryListing = iotAccess != null ? iotAccess.listDevices(registryId, null) : null; + List activeDevices = (registryListing == null || registryListing.device_ids == null) + ? List.of() + : registryListing.device_ids.entrySet().stream() + .filter(entry -> !isTrue(entry.getValue().blocked)) + .map(Entry::getKey) + .filter(this::matchesTarget) + .sorted() + .toList(); + + int activeCount = activeDevices.size(); + publishStartEvent(activeCount); + + if (shouldDetailEntries()) { + int eventNo = 0; + for (String deviceId : activeDevices) { + publishDeviceEvent(deviceId, ++eventNo); + } + } + + publishState(Phase.STOPPED, activeCount); + + processor.info("Completed model discovery for registry %s (%d active devices)", + registryId, activeCount); + } + + private boolean shouldDetailEntries() { + Depth depth = familyConfig != null ? familyConfig.depth : null; + return depth == null || depth == Depth.DETAILS || depth == Depth.PARTS; + } + + private boolean matchesTarget(String addr) { + List targets = familyConfig != null ? familyConfig.addrs : null; + return targets == null || targets.isEmpty() || (addr != null && targets.contains(addr)); + } + + private Envelope createDiscoveryEnvelope(SubType subType, String deviceId) { + Envelope envelope = new Envelope(); + envelope.deviceRegistryId = registryId; + envelope.deviceId = deviceId; + envelope.subType = subType; + envelope.subFolder = SubFolder.DISCOVERY; + envelope.source = MODEL_SOURCE; + return envelope; + } + + private DiscoveryEvents createDiscoveryEvent(int eventNo) { + DiscoveryEvents event = new DiscoveryEvents(); + event.version = UdmiServicePod.UDMI_VERSION; + event.timestamp = new Date(); + event.generation = generation; + event.event_no = eventNo; + event.family = ProtocolFamily.IOT; + return event; + } + + private void publishState(Phase phase, int activeCount) { + FamilyDiscoveryState familyState = new FamilyDiscoveryState(); + familyState.generation = generation; + familyState.phase = phase; + familyState.active_count = activeCount; + + DiscoveryState discoveryState = new DiscoveryState(); + discoveryState.version = UdmiServicePod.UDMI_VERSION; + discoveryState.timestamp = new Date(); + discoveryState.generation = generation; + discoveryState.families = new HashMap<>(); + discoveryState.families.put(ProtocolFamily.IOT, familyState); + + emitMessage(createDiscoveryEnvelope(SubType.STATE, null), discoveryState); + } + + private void publishStartEvent(int activeCount) { + DiscoveryEvents countEvent = createDiscoveryEvent(0); + countEvent.active_count = activeCount; + emitMessage(createDiscoveryEnvelope(SubType.EVENTS, null), countEvent); + } + + private void publishDeviceEvent(String deviceId, int eventNo) { + DiscoveryEvents deviceEvent = createDiscoveryEvent(eventNo); + deviceEvent.addr = deviceId; + + CloudModel deviceModel = iotAccess != null ? iotAccess.fetchDevice(registryId, deviceId) : null; + String metadataStr = (deviceModel != null && deviceModel.metadata != null) + ? deviceModel.metadata.get(MetadataMapKeys.UDMI_METADATA) + : null; + deviceEvent.model = (metadataStr != null && metadataStr.startsWith("{")) + ? fromString(Metadata.class, metadataStr) + : null; + + emitMessage(createDiscoveryEnvelope(SubType.EVENTS, deviceId), deviceEvent); + } + + private void emitMessage(Envelope envelope, Object payload) { + if (processor instanceof UufiProcessor uufiProcessor) { + uufiProcessor.publishModelDiscovery(envelope, payload); + return; + } + ifNotNullThen(UdmiServicePod.maybeGetComponent(UufiProcessor.class), + uufi -> uufi.publishModelDiscovery(envelope, payload)); + processor.publish(envelope, payload); + } +} diff --git a/udmis/src/main/java/com/google/bos/udmi/service/core/ReflectProcessor.java b/udmis/src/main/java/com/google/bos/udmi/service/core/ReflectProcessor.java index 84c3e0f9e5..8c0b0f1ac1 100644 --- a/udmis/src/main/java/com/google/bos/udmi/service/core/ReflectProcessor.java +++ b/udmis/src/main/java/com/google/bos/udmi/service/core/ReflectProcessor.java @@ -22,6 +22,7 @@ import static com.google.udmi.util.GeneralUtils.ifNotNullThen; import static com.google.udmi.util.GeneralUtils.ifNullThen; import static com.google.udmi.util.GeneralUtils.ifTrueThen; +import static com.google.udmi.util.GeneralUtils.isNotEmpty; import static com.google.udmi.util.GeneralUtils.multiTrim; import static com.google.udmi.util.GeneralUtils.requireNull; import static com.google.udmi.util.GeneralUtils.stackTraceString; @@ -61,6 +62,7 @@ import java.util.stream.Collectors; import udmi.schema.CloudModel; import udmi.schema.CloudModel.ModelOperation; +import udmi.schema.DiscoveryConfig; import udmi.schema.EndpointConfiguration; import udmi.schema.Entry; import udmi.schema.Envelope; @@ -298,11 +300,26 @@ private CloudModel queryDeviceState(Envelope attributes) { private CloudModel reflectModel(Envelope attributes, CloudModel request) { CloudModel modelResult = updateModel(attributes, request); - ifNotNullThen(extractModel(request), + Object extracted = extractModel(request); + ifNotNullThen(extracted, model -> publish(makeTargetEnvelope(attributes), model)); + if (request.resource_type == REGISTRY && request.operation != PREVIEW) { + String targetRegistry = getTargetRegistryId(attributes.deviceRegistryId, attributes.deviceId); + Date siteTimestamp = (extracted instanceof SiteMetadataUpdate smu) + ? smu.timestamp : request.timestamp; + Date generation = ofNullable(siteTimestamp).orElseGet(Date::new); + ModelDiscoveryHandler.processDiscovery(this, targetRegistry, null, generation); + } return modelResult; } + private String getTargetRegistryId(String registryId, String deviceId) { + if (isNotEmpty(deviceId) && (!isNotEmpty(registryId) || reflectRegistry.equals(registryId))) { + return deviceId; + } + return isNotEmpty(registryId) ? registryId : null; + } + private CloudModel updateModel(Envelope attributes, CloudModel request) { if (request.operation == PREVIEW) { // Nothing more to do in this case since this is just preview, meaning no impact. @@ -359,6 +376,16 @@ private CloudModel reflectProcess(Envelope attributes, Object payload) { if (payload == null) { return reply; } + if (attributes.subType == SubType.CONFIG && attributes.subFolder == SubFolder.DISCOVERY) { + DiscoveryConfig discoveryConfig = convertTo(DiscoveryConfig.class, payload); + if (ModelDiscoveryHandler.isModelDiscoveryConfig(attributes, discoveryConfig)) { + Envelope discEnv = deepCopy(attributes); + discEnv.deviceRegistryId = getTargetRegistryId( + attributes.deviceRegistryId, attributes.deviceId); + ModelDiscoveryHandler.processDiscoveryConfig(this, discEnv, discoveryConfig); + return reply; + } + } if (attributes.subType == SubType.CONFIG) { // If there was actually a config change, then the transaction will be acknowledged that way. ifNotNullThen(processConfigChange(attributes, payload, null), updated -> diff --git a/udmis/src/main/java/com/google/bos/udmi/service/core/TargetProcessor.java b/udmis/src/main/java/com/google/bos/udmi/service/core/TargetProcessor.java index 04556a85c0..9891556ea4 100644 --- a/udmis/src/main/java/com/google/bos/udmi/service/core/TargetProcessor.java +++ b/udmis/src/main/java/com/google/bos/udmi/service/core/TargetProcessor.java @@ -46,6 +46,11 @@ protected void defaultHandler(Object defaultedMessage) { publish(defaultedMessage); + if (ModelDiscoveryHandler.MODEL_SOURCE.equals(envelope.source)) { + debug("Skipping reflector forwarding for model discovery message"); + return; + } + if (deviceId == null) { notice("Dropping message with no deviceId: " + stringifyTerse(envelope)); return; diff --git a/udmis/src/main/java/com/google/bos/udmi/service/core/UufiProcessor.java b/udmis/src/main/java/com/google/bos/udmi/service/core/UufiProcessor.java index dbae389406..e4ce946739 100644 --- a/udmis/src/main/java/com/google/bos/udmi/service/core/UufiProcessor.java +++ b/udmis/src/main/java/com/google/bos/udmi/service/core/UufiProcessor.java @@ -16,6 +16,7 @@ import java.util.Objects; import java.util.Optional; import udmi.schema.CloudModel; +import udmi.schema.DiscoveryConfig; import udmi.schema.EndpointConfiguration; import udmi.schema.Envelope; import udmi.schema.Envelope.SubFolder; @@ -54,8 +55,12 @@ protected void defaultHandler(Object message) { // Message from MQTT (Client or Loopback) // Loopback protection using source attribute - if ("udmis".equals(envelope.source) || "udmis".equals(messageMap.get("source"))) { - debug("Ignoring loopback of message from udmis source"); + boolean isModelLoopback = (ModelDiscoveryHandler.MODEL_SOURCE.equals(envelope.source) + || ModelDiscoveryHandler.MODEL_SOURCE.equals(messageMap.get("source"))) + && envelope.subType != SubType.CONFIG; + if ("udmis".equals(envelope.source) || "udmis".equals(messageMap.get("source")) + || isModelLoopback) { + debug("Ignoring loopback of message from %s source", envelope.source); return; } @@ -166,6 +171,14 @@ private void handleUufiInbound(Envelope envelope, Map messageMap // Standard UDMI devices expect a complete config merged with previous state. if (innerEnvelope.subType == SubType.CONFIG) { + if (innerEnvelope.subFolder == SubFolder.DISCOVERY) { + DiscoveryConfig discoveryConfig = + catchToNull(() -> convertTo(DiscoveryConfig.class, innerPayload)); + if (ModelDiscoveryHandler.isModelDiscoveryConfig(innerEnvelope, discoveryConfig)) { + ModelDiscoveryHandler.processDiscoveryConfig(this, innerEnvelope, discoveryConfig); + return; + } + } String configUpdate = processConfigChange(innerEnvelope, innerPayload, null); if (configUpdate != null) { innerEnvelope.subFolder = null; @@ -177,6 +190,10 @@ private void handleUufiInbound(Envelope envelope, Map messageMap publish(innerEnvelope, innerPayload); } + void publishModelDiscovery(Envelope envelope, Object message) { + handleUufiOutbound(envelope, message); + } + private void handleUufiOutbound(Envelope envelope, Object message) { boolean isStateUpdate = envelope.subType == SubType.STATE && (envelope.subFolder == null || envelope.subFolder == SubFolder.UPDATE); @@ -231,10 +248,13 @@ private void handleUufiOutbound(Envelope envelope, Object message) { outboundMessage = payloadMap; } + String source = ModelDiscoveryHandler.MODEL_SOURCE.equals(envelope.source) + ? ModelDiscoveryHandler.MODEL_SOURCE : "udmis"; + // Wrap system message for UUFI clients Map uufiMessage = toMap(envelope); uufiMessage.put(PAYLOAD_KEY, outboundMessage); - uufiMessage.put("source", "udmis"); // For loopback protection + uufiMessage.put("source", source); // For loopback protection if (envelope.principal != null) { uufiMessage.put("principal", envelope.principal); } else { @@ -246,7 +266,7 @@ private void handleUufiOutbound(Envelope envelope, Object message) { uufiEnvelope.subFolder = envelope.subFolder; uufiEnvelope.deviceRegistryId = envelope.deviceRegistryId; uufiEnvelope.deviceId = envelope.deviceId; - uufiEnvelope.source = "udmis"; // Set udmis source + uufiEnvelope.source = source; uufiEnvelope.principal = envelope.principal; uufiEnvelope.gatewayId = "uufi"; diff --git a/udmis/src/main/java/com/google/bos/udmi/service/messaging/impl/SimpleMqttPipe.java b/udmis/src/main/java/com/google/bos/udmi/service/messaging/impl/SimpleMqttPipe.java index d65f96d5dc..a1f37fb2e8 100644 --- a/udmis/src/main/java/com/google/bos/udmi/service/messaging/impl/SimpleMqttPipe.java +++ b/udmis/src/main/java/com/google/bos/udmi/service/messaging/impl/SimpleMqttPipe.java @@ -176,13 +176,15 @@ private static Map parseUufiTopic(String topic) { int start = Strings.isNullOrEmpty(parts[0]) ? 1 : 0; Envelope envelope = new Envelope(); - // UUFI topic: [/namespace]/uufi/[r/REG/d/DEV/]c/TYPE/FOLDER + // UUFI topic: [/namespace]/uufi/[r/REG/[d/DEV/]]c/TYPE/FOLDER int base = start + 1; if ("r".equals(parts[base])) { envelope.deviceRegistryId = nullAsNull(parts[base + 1]); - checkState("d".equals(parts[base + 2]), "expected devices"); - envelope.deviceId = nullAsNull(parts[base + 3]); - base += 4; + base += 2; + if ("d".equals(parts[base])) { + envelope.deviceId = nullAsNull(parts[base + 1]); + base += 2; + } } checkState("c".equals(parts[base]), "expected commands"); envelope.subType = convertSubType(parts[base + 1]); diff --git a/udmis/src/test/java/com/google/bos/udmi/service/core/ReflectProcessorTest.java b/udmis/src/test/java/com/google/bos/udmi/service/core/ReflectProcessorTest.java index 71e2e91923..a58a494dcc 100644 --- a/udmis/src/test/java/com/google/bos/udmi/service/core/ReflectProcessorTest.java +++ b/udmis/src/test/java/com/google/bos/udmi/service/core/ReflectProcessorTest.java @@ -180,12 +180,24 @@ public void modelDeviceTest() { @Test public void modelRegistryUpdateTest() { + CloudModel registryDevices = new CloudModel(); + registryDevices.device_ids = Map.of("AHU-1", new CloudModel()); + when(provider.listDevices(eq(TEST_REGISTRY), any())).thenReturn(registryDevices); + + CloudModel deviceModel = new CloudModel(); + deviceModel.metadata = Map.of("udmi_metadata", + "{\"version\":\"1.5.2\",\"timestamp\":\"2026-07-28T00:00:00Z\"," + + "\"system\":{\"serial_no\":\"SN-AHU-1\"}}"); + when(provider.fetchDevice(eq(TEST_REGISTRY), eq("AHU-1"))).thenReturn(deviceModel); + CloudModel requestModel = new CloudModel(); requestModel.operation = ModelOperation.UPDATE; requestModel.resource_type = Resource_type.REGISTRY; activeTestInstance(() -> getReverseDispatcher().publish(makeModelBundle(requestModel))); verify(provider, times(1)).modelRegistry(eq(TEST_REGISTRY), any(), eq(requestModel)); + verify(provider, times(1)).listDevices(eq(TEST_REGISTRY), any()); + verify(provider, times(1)).fetchDevice(eq(TEST_REGISTRY), eq("AHU-1")); } @Test diff --git a/udmis/src/test/java/com/google/bos/udmi/service/core/UufiProcessorTest.java b/udmis/src/test/java/com/google/bos/udmi/service/core/UufiProcessorTest.java index 69c3620b0a..d5f48d2190 100644 --- a/udmis/src/test/java/com/google/bos/udmi/service/core/UufiProcessorTest.java +++ b/udmis/src/test/java/com/google/bos/udmi/service/core/UufiProcessorTest.java @@ -323,6 +323,155 @@ public void outboundMonolithicStateShardingTest() { assertEquals(expectedSubFolders, subFolders, "sharded subfolders mismatch"); } + /** + * Test on-demand Model Discovery via UUFI config/discovery. + */ + @Test + public void modelDiscoveryTest() { + udmi.schema.CloudModel registryModel = new udmi.schema.CloudModel(); + registryModel.device_ids = Map.of( + "AHU-1", new udmi.schema.CloudModel(), + "GAT-123", new udmi.schema.CloudModel() + ); + org.mockito.Mockito.when(provider.listDevices(org.mockito.ArgumentMatchers.eq("reg-1"), any())) + .thenReturn(registryModel); + + udmi.schema.CloudModel ahuModel = new udmi.schema.CloudModel(); + ahuModel.metadata = Map.of("udmi_metadata", + "{\"version\":\"1.5.2\",\"timestamp\":\"2026-07-28T00:00:00Z\"," + + "\"system\":{\"serial_no\":\"SN-AHU-1\"}}"); + udmi.schema.CloudModel gatModel = new udmi.schema.CloudModel(); + gatModel.metadata = Map.of("udmi_metadata", + "{\"version\":\"1.5.2\",\"timestamp\":\"2026-07-28T00:00:00Z\"," + + "\"system\":{\"serial_no\":\"SN-GAT-123\"}}"); + org.mockito.Mockito.when(provider.fetchDevice("reg-1", "AHU-1")).thenReturn(ahuModel); + org.mockito.Mockito.when(provider.fetchDevice("reg-1", "GAT-123")).thenReturn(gatModel); + + Envelope innerEnvelope = new Envelope(); + innerEnvelope.subType = SubType.CONFIG; + innerEnvelope.subFolder = SubFolder.DISCOVERY; + innerEnvelope.deviceRegistryId = "reg-1"; + + Map uufiWrapper = toMap(innerEnvelope); + uufiWrapper.put("payload", Map.of( + "version", "1.5.2", + "timestamp", "2026-07-28T00:00:00Z", + "families", Map.of("iot", Map.of( + "generation", "2026-07-28T00:00:00Z", + "depth", "details", + "source", "model" + )) + )); + + Envelope transportEnvelope = new Envelope(); + transportEnvelope.source = "test-client"; + transportEnvelope.gatewayId = "uufi"; + transportEnvelope.deviceRegistryId = "reg-1"; + transportEnvelope.subType = SubType.CONFIG; + transportEnvelope.subFolder = SubFolder.DISCOVERY; + + activeTestInstance(() -> getReverseDispatcher().publish( + new Bundle(transportEnvelope, uufiWrapper))); + + // 4 messages published by ModelDiscoveryHandler: + // 1. DiscoveryState (active, active_count=0) + // 2. DiscoveryEvents (event_no=0, active_count=2) + // 3. DiscoveryEvents (event_no=1, addr=AHU-1, model=...) + // 4. DiscoveryEvents (event_no=2, addr=GAT-123, model=...) + // 5. DiscoveryState (stopped, active_count=2) + assertEquals(5, captured.size(), "Expected 5 model discovery messages"); + + Map msg0 = toMap(captured.get(0)); + assertEquals("model", msg0.get("source")); + udmi.schema.DiscoveryState startState = + convertTo(udmi.schema.DiscoveryState.class, msg0.get("payload")); + assertEquals(udmi.schema.FamilyDiscoveryState.Phase.ACTIVE, + startState.families.get("iot").phase); + assertEquals(0, startState.families.get("iot").active_count); + + Map msg1 = toMap(captured.get(1)); + assertEquals("model", msg1.get("source")); + udmi.schema.DiscoveryEvents startEvent = + convertTo(udmi.schema.DiscoveryEvents.class, msg1.get("payload")); + assertEquals(0, startEvent.event_no); + assertEquals(2, startEvent.active_count); + assertEquals("iot", startEvent.family); + + Map msg2 = toMap(captured.get(2)); + assertEquals("AHU-1", msg2.get("deviceId")); + udmi.schema.DiscoveryEvents devEvent1 = + convertTo(udmi.schema.DiscoveryEvents.class, msg2.get("payload")); + assertEquals(1, devEvent1.event_no); + assertEquals("AHU-1", devEvent1.addr); + assertNotNull(devEvent1.model); + assertEquals("SN-AHU-1", devEvent1.model.system.serial_no); + + Map msg3 = toMap(captured.get(3)); + assertEquals("GAT-123", msg3.get("deviceId")); + udmi.schema.DiscoveryEvents devEvent2 = + convertTo(udmi.schema.DiscoveryEvents.class, msg3.get("payload")); + assertEquals(2, devEvent2.event_no); + assertEquals("GAT-123", devEvent2.addr); + assertNotNull(devEvent2.model); + assertEquals("SN-GAT-123", devEvent2.model.system.serial_no); + + Map msg4 = toMap(captured.get(4)); + udmi.schema.DiscoveryState stopState = + convertTo(udmi.schema.DiscoveryState.class, msg4.get("payload")); + assertEquals(udmi.schema.FamilyDiscoveryState.Phase.STOPPED, + stopState.families.get("iot").phase); + assertEquals(2, stopState.families.get("iot").active_count); + } + + /** + * Test Model Discovery with depth=entries (count only) and addrs filter. + */ + @Test + public void modelDiscoveryEntriesAndAddrsTest() { + udmi.schema.CloudModel registryModel = new udmi.schema.CloudModel(); + registryModel.device_ids = Map.of( + "AHU-1", new udmi.schema.CloudModel(), + "GAT-123", new udmi.schema.CloudModel() + ); + org.mockito.Mockito.when(provider.listDevices(org.mockito.ArgumentMatchers.eq("reg-1"), any())) + .thenReturn(registryModel); + + Envelope innerEnvelope = new Envelope(); + innerEnvelope.subType = SubType.CONFIG; + innerEnvelope.subFolder = SubFolder.DISCOVERY; + innerEnvelope.deviceRegistryId = "reg-1"; + + Map uufiWrapper = toMap(innerEnvelope); + uufiWrapper.put("payload", Map.of( + "version", "1.5.2", + "timestamp", "2026-07-28T00:00:00Z", + "families", Map.of("iot", Map.of( + "generation", "2026-07-28T00:00:00Z", + "depth", "entries", + "addrs", List.of("AHU-1"), + "source", "model" + )) + )); + + Envelope transportEnvelope = new Envelope(); + transportEnvelope.source = "test-client"; + transportEnvelope.gatewayId = "uufi"; + transportEnvelope.deviceRegistryId = "reg-1"; + transportEnvelope.subType = SubType.CONFIG; + transportEnvelope.subFolder = SubFolder.DISCOVERY; + + activeTestInstance(() -> getReverseDispatcher().publish( + new Bundle(transportEnvelope, uufiWrapper))); + + // With depth=entries, only 3 messages: start state, event_no=0 count, stop state + assertEquals(3, captured.size(), "Expected 3 messages for depth=entries"); + Map msg1 = toMap(captured.get(1)); + udmi.schema.DiscoveryEvents startEvent = + convertTo(udmi.schema.DiscoveryEvents.class, msg1.get("payload")); + assertEquals(0, startEvent.event_no); + assertEquals(1, startEvent.active_count); + } + private UufiProcessor getProcessor() { return (UufiProcessor) UdmiServicePod.getComponent(UufiProcessor.class); } diff --git a/udmis/src/test/java/com/google/bos/udmi/service/messaging/impl/SimpleMqttPipeTest.java b/udmis/src/test/java/com/google/bos/udmi/service/messaging/impl/SimpleMqttPipeTest.java index 3f714fcd4f..d9b4436bec 100644 --- a/udmis/src/test/java/com/google/bos/udmi/service/messaging/impl/SimpleMqttPipeTest.java +++ b/udmis/src/test/java/com/google/bos/udmi/service/messaging/impl/SimpleMqttPipeTest.java @@ -112,5 +112,20 @@ public void staticEnvelopeTopic() { assertEquals("events", legacyPointset.get(SUBTYPE_PROPERTY_KEY)); assertEquals("pointset", legacyPointset.get(SUBFOLDER_PROPERTY_KEY)); assertEquals(3, legacyPointset.keySet().size()); + + Map uufiRegistry = parseEnvelopeTopic("/uufi/r/my-reg/c/config/discovery"); + assertEquals("my-reg", uufiRegistry.get(REGISTRY_ID_PROPERTY_KEY)); + assertNull(uufiRegistry.get(DEVICE_ID_KEY)); + assertEquals("config", uufiRegistry.get(SUBTYPE_PROPERTY_KEY)); + assertEquals("discovery", uufiRegistry.get(SUBFOLDER_PROPERTY_KEY)); + assertEquals("uufi", uufiRegistry.get(GATEWAY_ID_KEY)); + + Map uufiDevice = + parseEnvelopeTopic("/uufi/r/my-reg/d/my-dev/c/events/discovery"); + assertEquals("my-reg", uufiDevice.get(REGISTRY_ID_PROPERTY_KEY)); + assertEquals("my-dev", uufiDevice.get(DEVICE_ID_KEY)); + assertEquals("events", uufiDevice.get(SUBTYPE_PROPERTY_KEY)); + assertEquals("discovery", uufiDevice.get(SUBFOLDER_PROPERTY_KEY)); + assertEquals("uufi", uufiDevice.get(GATEWAY_ID_KEY)); } } \ No newline at end of file diff --git a/validator/src/main/java/com/google/daq/mqtt/registrar/Registrar.java b/validator/src/main/java/com/google/daq/mqtt/registrar/Registrar.java index b00bb6cb28..315d4966c0 100644 --- a/validator/src/main/java/com/google/daq/mqtt/registrar/Registrar.java +++ b/validator/src/main/java/com/google/daq/mqtt/registrar/Registrar.java @@ -410,8 +410,8 @@ protected void execute(Runnable modelMunger) { if (createRegistries >= 0) { createRegistries(); } else { - processSiteMetadata(); processAllDevices(modelMunger); + processSiteMetadata(); } writeErrors(); } catch (ExceptionMap em) { From c5ff1af61ad1ac4502663b0abf8a04d89e237d5e Mon Sep 17 00:00:00 2001 From: Khyati Mahendru Date: Mon, 19 Oct 2026 09:33:21 +0000 Subject: [PATCH 2/7] feat(discovery): emit model discovery to udmi_target with transactionId and source --- .gencode_hash.txt | 16 ++-- bin/test_uufi_client | 23 +++-- docs/specs/discovery.md | 8 +- gencode/docs/events.html | 41 +++++++++ gencode/docs/events_discovery.html | 89 +++++++++++++++++++ gencode/docs/metadata.html | 48 ++++++++++ gencode/docs/state.html | 55 ++++++++++++ gencode/java/udmi/schema/DiscoveryEvents.java | 10 ++- .../udmi/schema/FamilyDiscoveryState.java | 10 ++- .../python/udmi/schema/events_discovery.py | 1 + .../udmi/schema/state_discovery_family.py | 1 + schema/events_discovery.json | 4 + schema/state_discovery_family.json | 4 + .../service/core/ModelDiscoveryHandler.java | 21 ++--- .../udmi/service/core/TargetProcessor.java | 5 +- .../bos/udmi/service/core/UufiProcessor.java | 19 +--- .../messaging/impl/SimpleMqttPipe.java | 4 +- .../service/core/ReflectProcessorTest.java | 7 ++ .../udmi/service/core/UufiProcessorTest.java | 29 +++--- 19 files changed, 320 insertions(+), 75 deletions(-) diff --git a/.gencode_hash.txt b/.gencode_hash.txt index 0ba4f3f7b4..cf376b1f3b 100644 --- a/.gencode_hash.txt +++ b/.gencode_hash.txt @@ -7,15 +7,15 @@ c21266e8de23b6e8acfac16a177db782375bf71df470c343c3f8eb8b822ef56a gencode/docs/c 94bb566ac6a4b6b7f89cd2cf1911f658446cc0bea405014275104623f9964dd3 gencode/docs/configuration_pubber.html 1057fa40fb7a31a23bb2773d21c38cf4590a935bd8b5ea4218e695c6204f5dd9 gencode/docs/data_template.html 1d5294b4a1fea6555e23144e594523e4793c795069b6880ce607ad09a6015581 gencode/docs/equipment_translation.html -248dd071e4981e17dfe2e2a455e21c43ba179fa1141d00be33ded36838bcd9f3 gencode/docs/events.html +ad23d7fa3cf403b35565c7058f68c41f02c3e7c10614e95dcafa69f53b1e2c08 gencode/docs/events.html 70e57ad6ef39330d958727ebf9dcd61ef6ea30e4c8653eac412bf1867fdb3a70 gencode/docs/events_alarmset.html -59cc1e456b56b7cccf41c5f87b2632cec0a69b021f871145fa56d0e4528196ee gencode/docs/events_discovery.html +03b1c67910c9244476ab62e11ebc3c2c8c164e3b4f289f96446b1a3a24f62373 gencode/docs/events_discovery.html eafcc3c48189f605f114cde051fea9d13fc5f1a3e395d64fb0a91cb53d4c9aeb gencode/docs/events_pointset.html b65ce6e8061e61ba676a72c624f1c425bd238e152ce8e3ebb1e9c23f0e502bcb gencode/docs/events_streams.html 81043ba732caab75704d38e6977b32b1c2bcdad89d94ad2a259d0e70ca4aa40e gencode/docs/events_system.html 46fd37b577c655965346d1e3ececf559c4384791214900eeeea1ad8fc72a02b8 gencode/docs/events_udmi.html baf59590b1a71a49e59d8f499ae5112a1b7a555294dbd72bc821203aecceefa3 gencode/docs/events_validation.html -a4feddcbec98f08c316b4dd529e28b588dd255bf4fc15023f0a631d99a77bc6f gencode/docs/metadata.html +5141c93cffcd2e00e47538b8a0f1fc11eaa7706756e8ccdd1b4c803b8167b4ef gencode/docs/metadata.html 18178a591940c176371fb9196048eaacd40f1323997cf907ca11eb879a095c7e gencode/docs/monitoring.html 64d21cf916e20566171cf9d723e59c297b576bcba89e2a73aec2b018398cc7da gencode/docs/mosquitto_client_response.html 6f332da9cc64230df3551f9748a4eaa4060146e767cae967fc0e73b1f348be5b gencode/docs/persistent_device.html @@ -26,7 +26,7 @@ eb6c623142496b61e3c9b00c9ead0ba8541af2050d76ce652e1721c84aa3a096 gencode/docs/r 165a29291d7d66653e46fd1ac5868f63e18f9d67c06ea9ebcb95d4830a528e68 gencode/docs/schema_doc.min.js 7ed934930aee763e0beebc349725ba3909115e8d346bb762f28bcbe745bb163a gencode/docs/schema_extras.js 7a1bbe014c739078d354ec484147325e1d0bd19468f275a53da38add43ebe089 gencode/docs/site_metadata.html -0993d21a1a0ae485bdb2f1794f279d13f782e841d07832f795624cc9f158ea93 gencode/docs/state.html +9bf177ae485395a95661f7deedbbefa54779de8d06337c039cafce145fc59b5f gencode/docs/state.html 275171b2329d64d497fa54f8877656920be7c4f79bc90f67a0ff1bae6700399b gencode/docs/state_validation.html 46347b31bc2a56bc74af9bb702ad40c7823887d5e9b1427903a4892cde3cd169 gencode/java/udmi/schema/AlarmAlarmsetConfig.java 520b12eb8cec4f043f81f898c4be12def09b7de138533922e17c038415fb8902 gencode/java/udmi/schema/AlarmAlarmsetEvents.java @@ -66,7 +66,7 @@ c04302f973508295258d4d3a29b5da70bf3cd3a6e2c63a958fb70dd3d370276c gencode/java/u 52ae2afb2dcb7863259e7eea71888811663c010429c09760493f5f006a660c84 gencode/java/udmi/schema/Dimension.java 2bb1513817eea9c0486b28f3316ead16d53885a8bad9f6a42ee3d5f762335a68 gencode/java/udmi/schema/DiscoveryCommand.java a572f17a61584a32ff19968356f7f7d7bcc8430131ac3701878d0bd221e1a1fa gencode/java/udmi/schema/DiscoveryConfig.java -ebc57084db00ed858c8bebcf926efc39c18ff345d9b5ca963bf52118635e6832 gencode/java/udmi/schema/DiscoveryEvents.java +125646ad6cd48e61b743ed0342283b42efba15f072bbdb882c6d9871930154fd gencode/java/udmi/schema/DiscoveryEvents.java af16026c143a592562b77012dd739c02364bd1a9d6bbd6c7ede4f3a5a38b7f4a gencode/java/udmi/schema/DiscoveryModel.java 0fd348086bfd8e38a54a06b0fd205f6d01fdc6236f07ccc8bef49a1832f8c767 gencode/java/udmi/schema/DiscoveryState.java baef666f5f58f790b73ba7bed8f60eacdd75ba3151b9abc32265954a9da84faa gencode/java/udmi/schema/Electricity.java @@ -79,7 +79,7 @@ f890a14567657f5c97c2d3c1c753d751025ee094700ef0659f63f448529cc529 gencode/java/u a5a41a2bc809988efbbc121cdf730cbc1eb066372e046747fb6ae3f30e96a8b1 gencode/java/udmi/schema/FamilyDiscovery.java 60286ff934d1ad721d90b2691538700073b73376207b06badfbd3f65e9d51b29 gencode/java/udmi/schema/FamilyDiscoveryConfig.java 6f9eaf4b56d3666caa65be4ded3344cb5b59aa9f838b454b5a4983d68061163e gencode/java/udmi/schema/FamilyDiscoveryModel.java -1d1f5a6ebf1db5f06976c6e4fb14f1404eec5460b9eb34f0508e1f95aa69e81b gencode/java/udmi/schema/FamilyDiscoveryState.java +9a786b1f9a4815f108e805ce0ffadea01a1577a4d051ba70d391f82a726c5f2f gencode/java/udmi/schema/FamilyDiscoveryState.java 180a65a6c61b76cbec7bb45bbfdfe0d4f26170d7f7e74aef9bd129f1dd451edf gencode/java/udmi/schema/FamilyLocalnetConfig.java d6782eef0f60aa8dc9f57219ba6dd336a1a9d68e7ba174e59c7dfd6f0f05a4f4 gencode/java/udmi/schema/FamilyLocalnetModel.java 9ca698d84b784b87d52d1f6cbaaa08964a7f050dfefe338ea95afc440ccc9a9f gencode/java/udmi/schema/FamilyLocalnetState.java @@ -203,7 +203,7 @@ ab1a8ee1f12d04dd88837b6f026030b5ed3550ef3bfbbfbcb4f77cc4df39e93a gencode/python 3dcf4daa94b8450b7360e58500e20e1fbe5701502ac28358112ee1a6bd2a87f1 gencode/python/udmi/schema/events.py af48a2d32b4ec326df6e3f165d8944b994fb426bd4e663c42c1d592f5cb41f83 gencode/python/udmi/schema/events_alarmset.py b58371ffa7979d1598502a35859f3010ae7a2ec2d851454c010c835e426c7a14 gencode/python/udmi/schema/events_alarmset_alarm.py -ad1e97abeee523cdfe5c1c560f44da356eb30ef4cb1c464ef75e32bfbecbb6fc gencode/python/udmi/schema/events_discovery.py +46ed7eb98290eee6a2a3b271be75269b6dc2d5a439c61150384c2f1efe9f68bb gencode/python/udmi/schema/events_discovery.py 84700f9d8a8975130ff9c62891003f7a12f367fa7dd1ca1db5f5fae7225bf005 gencode/python/udmi/schema/events_pointset.py 9f568bd573d4992e3efa3cc3cb8cdefecb95b15675b0442873abdc7850d108f9 gencode/python/udmi/schema/events_pointset_point.py 282439e4983ae9f310fda69dd6ff979567fa983ebdefcde407df42d631fe1bad gencode/python/udmi/schema/events_streams.py @@ -249,7 +249,7 @@ bc7fae9084e9511001e87409867ea5c1edd29892b21cd611d7d3689afa39ed68 gencode/python c3aafb1bcd85fc43f50aaba1f30f51657108d70ef62ffd95444112be508973bb gencode/python/udmi/schema/state_blobset.py a3b9848f8887b75e013276ebd353afad899f147bec7c1ea0d70d7833f5dbe212 gencode/python/udmi/schema/state_blobset_blob.py 450d9ef8aca6ac904a8231ab4b0d1c268f0f5d24693fd4757664bb4c71e4a7f6 gencode/python/udmi/schema/state_discovery.py -9332d44d29f5c6d5642aadc5564f83c39556eb94bfc55559eca955d2d53ea4ab gencode/python/udmi/schema/state_discovery_family.py +b9ec341b956f7b71b28b72ad372eca66e4d728670756b1c535fceb47993cdbc6 gencode/python/udmi/schema/state_discovery_family.py ea2dc5ef443f91f2f6a6b5a0acc09e791870a275840a41acbb9bff82c3a4b8ee gencode/python/udmi/schema/state_gateway.py 2ca909da04032af58498600234f0a0c677c0f83b4705f4899bfb630b7aec05af gencode/python/udmi/schema/state_localnet.py e3763677989966c0884163bb726094c759ec4f2eb5c103880cecd895a3315fd8 gencode/python/udmi/schema/state_localnet_family.py diff --git a/bin/test_uufi_client b/bin/test_uufi_client index 7c41824823..e98252e070 100755 --- a/bin/test_uufi_client +++ b/bin/test_uufi_client @@ -200,17 +200,14 @@ echo "System Model Update SUCCESS: Received matching model update message on bus echo "Starting Site Model Discovery test..." DISC_LOG_FILE=$(mktemp) DISC_GEN=$(date -u +%Y-%m-%dT%H:%M:%SZ) -echo "Subscribing to /uufi/r/$REGISTRY_ID/# for Model Discovery sequence (gen: $DISC_GEN)..." -mosquitto_sub $MQTT_ARGS -i "${CLIENT_ID}_disc_sub" -t "/uufi/r/$REGISTRY_ID/#" -F "%t %p" | while read -r topic payload; do - if [[ "$topic" == *"/discovery" ]]; then - echo "$topic $payload" >> "$DISC_LOG_FILE" - SUB_TYPE=$(echo "$payload" | jq -r .subType 2>/dev/null) - PHASE=$(echo "$payload" | jq -r .payload.families.iot.phase 2>/dev/null) - GEN=$(echo "$payload" | jq -r .payload.families.iot.generation 2>/dev/null) - if [[ "$SUB_TYPE" == "state" && "$PHASE" == "stopped" && "$GEN" == "$DISC_GEN" ]]; then - kill -USR1 $$ 2>/dev/null || true - exit 0 - fi +echo "Subscribing to /r/$REGISTRY_ID/d/+/+/discovery for Model Discovery sequence (gen: $DISC_GEN)..." +mosquitto_sub $MQTT_ARGS -i "${CLIENT_ID}_disc_sub" -t "/r/$REGISTRY_ID/d/+/+/discovery" -F "%t %p" | while read -r topic payload; do + echo "$topic $payload" >> "$DISC_LOG_FILE" + PHASE=$(echo "$payload" | jq -r .families.iot.phase 2>/dev/null) + GEN=$(echo "$payload" | jq -r .families.iot.generation 2>/dev/null) + if [[ "$topic" == *"/state/discovery" && "$PHASE" == "stopped" && "$GEN" == "$DISC_GEN" ]]; then + kill -USR1 $$ 2>/dev/null || true + exit 0 fi done & SUB_DISC_PID=$! @@ -238,10 +235,10 @@ if [ $GOT_DISC -eq 0 ]; then exit 1 fi -# Verify active state, event_no=0 count, AHU-1 model event, and stopped state +# Verify active state, event_no=0 count, AHU-1 model event, source:model, and stopped state on target stream grep -q '"phase":"active"' "$DISC_LOG_FILE" || { echo "ERROR: Missing active DiscoveryState in Model Discovery" >&2; cat "$DISC_LOG_FILE"; exit 1; } grep -q '"event_no":0' "$DISC_LOG_FILE" || { echo "ERROR: Missing event_no=0 DiscoveryEvents in Model Discovery" >&2; cat "$DISC_LOG_FILE"; exit 1; } -grep -q "/uufi/r/$REGISTRY_ID/d/$DUT_ID/c/events/discovery" "$DISC_LOG_FILE" || { echo "ERROR: Missing $DUT_ID DiscoveryEvents in Model Discovery" >&2; cat "$DISC_LOG_FILE"; exit 1; } +grep -q "/r/$REGISTRY_ID/d/$DUT_ID/events/discovery" "$DISC_LOG_FILE" || { echo "ERROR: Missing $DUT_ID DiscoveryEvents in Model Discovery" >&2; cat "$DISC_LOG_FILE"; exit 1; } grep -q '"source":"model"' "$DISC_LOG_FILE" || { echo "ERROR: Missing source:model in Model Discovery messages" >&2; cat "$DISC_LOG_FILE"; exit 1; } grep -q '"phase":"stopped"' "$DISC_LOG_FILE" || { echo "ERROR: Missing stopped DiscoveryState in Model Discovery" >&2; cat "$DISC_LOG_FILE"; exit 1; } echo "Site Model Discovery SUCCESS: Verified start state, event_no=0 count, device model events, and stop state" diff --git a/docs/specs/discovery.md b/docs/specs/discovery.md index 9a2e6cedcc..a878ea3d2a 100644 --- a/docs/specs/discovery.md +++ b/docs/specs/discovery.md @@ -82,11 +82,11 @@ be reported: ## Site Model Discovery -Site Model Discovery exposes the cloud-registered device models of a registry over the standard UDMI discovery pipeline (`family: "iot"`, `source: "model"`). It is triggered automatically when a registry model update is processed by UDMIS, or on-demand by publishing a registry-scoped [`config`](../../gencode/docs/config.html) discovery message (e.g., on `/uufi/r/{deviceRegistryId}/c/config/discovery` with `families.iot.source` set to `"model"`). +Site Model Discovery exposes the cloud-registered device models of a registry over the standard UDMI discovery pipeline (`family: "iot"`, `source: "model"`). It is triggered automatically when a registry model update is processed by UDMIS, or on-demand by publishing a registry-scoped [`config`](../../gencode/docs/config.html) discovery message with `families.iot.source` set to `"model"`. -The Site Model Discovery sequence follows the standard discovery state and event bookends: +The Site Model Discovery sequence is published to the `udmi_target` stream with envelope attributes `source: "model"` and `transactionId` set to the `generation` timestamp, and follows the standard discovery state and event bookends: -1. **Start State (`state/discovery`):** Published at the registry level with `source: "model"`, `families.iot.phase: "active"`, and `families.iot.active_count: 0`. +1. **Start State (`state/discovery`):** Published at the registry level with `families.iot.source: "model"`, `families.iot.phase: "active"`, and `families.iot.active_count: 0`. 2. **Start & Count Event (`events/discovery`):** Published at the registry level with `source: "model"`, `event_no: 0`, `family: "iot"`, and `active_count` set to the total number of matching devices in the registry. 3. **Device Model Events (`events/discovery`):** When `depth` is `details` (or `parts`, or omitted), one event per device (`event_no: 1..N`) is published with `source: "model"`, `family: "iot"`, `addr` set to the device ID, and `model` populated with the device's [`metadata`](../../gencode/docs/metadata.html) object. When `depth` is `entries`, individual device events are omitted and only the `event_no: 0` count is emitted. If `addrs` is specified in `families.iot`, only matching device IDs are included. -4. **Stop State (`state/discovery`):** Published at the registry level with `source: "model"`, `families.iot.phase: "stopped"`, and `families.iot.active_count` set to the total number of discovered devices. +4. **Stop State (`state/discovery`):** Published at the registry level with `families.iot.source: "model"`, `families.iot.phase: "stopped"`, and `families.iot.active_count` set to the total number of discovered devices. diff --git a/gencode/docs/events.html b/gencode/docs/events.html index c784050761..35cdc54037 100644 --- a/gencode/docs/events.html +++ b/gencode/docs/events.html @@ -6623,6 +6623,47 @@

+

+ + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Optional source qualifier for the discovery scan (e.g., 'model')

+
+ + + + + +
diff --git a/gencode/docs/events_discovery.html b/gencode/docs/events_discovery.html index 351352b419..ed276bf4fc 100644 --- a/gencode/docs/events_discovery.html +++ b/gencode/docs/events_discovery.html @@ -2314,6 +2314,40 @@

+

+ + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Optional source qualifier for the discovery scan (e.g., 'model')

+
+ + + + + +
@@ -46938,6 +46972,61 @@

+

+ + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Optional source qualifier for the discovery scan (e.g., 'model')

+
+ + + + + +
diff --git a/gencode/docs/metadata.html b/gencode/docs/metadata.html index 137e32b2eb..640cf801a7 100644 --- a/gencode/docs/metadata.html +++ b/gencode/docs/metadata.html @@ -24882,6 +24882,54 @@

+

+ + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Optional source qualifier for the discovery scan (e.g., 'model')

+
+ + + + + +
diff --git a/gencode/docs/state.html b/gencode/docs/state.html index 99a913102d..0a52457c03 100644 --- a/gencode/docs/state.html +++ b/gencode/docs/state.html @@ -6389,6 +6389,61 @@

Value must be greater or equal to 0

+

+ + + +
+
+
+

+ +

+
+ +
+
+ + Type: string
+

Optional source qualifier for the discovery scan (e.g., 'model')

+
+ + + + + +
diff --git a/gencode/java/udmi/schema/DiscoveryEvents.java b/gencode/java/udmi/schema/DiscoveryEvents.java index 48148bac41..095c97b60d 100644 --- a/gencode/java/udmi/schema/DiscoveryEvents.java +++ b/gencode/java/udmi/schema/DiscoveryEvents.java @@ -49,6 +49,13 @@ public class DiscoveryEvents { public Entry status; @JsonProperty("family") public String family; + /** + * Optional source qualifier for the discovery scan (e.g., 'model') + * + */ + @JsonProperty("source") + @JsonPropertyDescription("Optional source qualifier for the discovery scan (e.g., 'model')") + public java.lang.String source; /** * The primary address of the device (for indicated family) * @@ -151,6 +158,7 @@ public int hashCode() { int result = 1; result = ((result* 31)+((this.generation == null)? 0 :this.generation.hashCode())); result = ((result* 31)+((this.devices == null)? 0 :this.devices.hashCode())); + result = ((result* 31)+((this.source == null)? 0 :this.source.hashCode())); result = ((result* 31)+((this.active_count == null)? 0 :this.active_count.hashCode())); result = ((result* 31)+((this.registries == null)? 0 :this.registries.hashCode())); result = ((result* 31)+((this.families == null)? 0 :this.families.hashCode())); @@ -179,7 +187,7 @@ public boolean equals(Object other) { return false; } DiscoveryEvents rhs = ((DiscoveryEvents) other); - return (((((((((((((((((((this.generation == rhs.generation)||((this.generation!= null)&&this.generation.equals(rhs.generation)))&&((this.devices == rhs.devices)||((this.devices!= null)&&this.devices.equals(rhs.devices))))&&((this.active_count == rhs.active_count)||((this.active_count!= null)&&this.active_count.equals(rhs.active_count))))&&((this.registries == rhs.registries)||((this.registries!= null)&&this.registries.equals(rhs.registries))))&&((this.families == rhs.families)||((this.families!= null)&&this.families.equals(rhs.families))))&&((this.version == rhs.version)||((this.version!= null)&&this.version.equals(rhs.version))))&&((this.network == rhs.network)||((this.network!= null)&&this.network.equals(rhs.network))))&&((this.points == rhs.points)||((this.points!= null)&&this.points.equals(rhs.points))))&&((this.features == rhs.features)||((this.features!= null)&&this.features.equals(rhs.features))))&&((this.system == rhs.system)||((this.system!= null)&&this.system.equals(rhs.system))))&&((this.event_no == rhs.event_no)||((this.event_no!= null)&&this.event_no.equals(rhs.event_no))))&&((this.refs == rhs.refs)||((this.refs!= null)&&this.refs.equals(rhs.refs))))&&((this.model == rhs.model)||((this.model!= null)&&this.model.equals(rhs.model))))&&((this.family == rhs.family)||((this.family!= null)&&this.family.equals(rhs.family))))&&((this.addr == rhs.addr)||((this.addr!= null)&&this.addr.equals(rhs.addr))))&&((this.cloud_model == rhs.cloud_model)||((this.cloud_model!= null)&&this.cloud_model.equals(rhs.cloud_model))))&&((this.timestamp == rhs.timestamp)||((this.timestamp!= null)&&this.timestamp.equals(rhs.timestamp))))&&((this.status == rhs.status)||((this.status!= null)&&this.status.equals(rhs.status)))); + return ((((((((((((((((((((this.generation == rhs.generation)||((this.generation!= null)&&this.generation.equals(rhs.generation)))&&((this.devices == rhs.devices)||((this.devices!= null)&&this.devices.equals(rhs.devices))))&&((this.source == rhs.source)||((this.source!= null)&&this.source.equals(rhs.source))))&&((this.active_count == rhs.active_count)||((this.active_count!= null)&&this.active_count.equals(rhs.active_count))))&&((this.registries == rhs.registries)||((this.registries!= null)&&this.registries.equals(rhs.registries))))&&((this.families == rhs.families)||((this.families!= null)&&this.families.equals(rhs.families))))&&((this.version == rhs.version)||((this.version!= null)&&this.version.equals(rhs.version))))&&((this.network == rhs.network)||((this.network!= null)&&this.network.equals(rhs.network))))&&((this.points == rhs.points)||((this.points!= null)&&this.points.equals(rhs.points))))&&((this.features == rhs.features)||((this.features!= null)&&this.features.equals(rhs.features))))&&((this.system == rhs.system)||((this.system!= null)&&this.system.equals(rhs.system))))&&((this.event_no == rhs.event_no)||((this.event_no!= null)&&this.event_no.equals(rhs.event_no))))&&((this.refs == rhs.refs)||((this.refs!= null)&&this.refs.equals(rhs.refs))))&&((this.model == rhs.model)||((this.model!= null)&&this.model.equals(rhs.model))))&&((this.family == rhs.family)||((this.family!= null)&&this.family.equals(rhs.family))))&&((this.addr == rhs.addr)||((this.addr!= null)&&this.addr.equals(rhs.addr))))&&((this.cloud_model == rhs.cloud_model)||((this.cloud_model!= null)&&this.cloud_model.equals(rhs.cloud_model))))&&((this.timestamp == rhs.timestamp)||((this.timestamp!= null)&&this.timestamp.equals(rhs.timestamp))))&&((this.status == rhs.status)||((this.status!= null)&&this.status.equals(rhs.status)))); } } diff --git a/gencode/java/udmi/schema/FamilyDiscoveryState.java b/gencode/java/udmi/schema/FamilyDiscoveryState.java index b60f9e9e45..b406d3ad3f 100644 --- a/gencode/java/udmi/schema/FamilyDiscoveryState.java +++ b/gencode/java/udmi/schema/FamilyDiscoveryState.java @@ -49,6 +49,13 @@ public class FamilyDiscoveryState { @JsonProperty("passive_count") @JsonPropertyDescription("Number of passive scan results currently 'on hold'") public Integer passive_count; + /** + * Optional source qualifier for the discovery scan (e.g., 'model') + * + */ + @JsonProperty("source") + @JsonPropertyDescription("Optional source qualifier for the discovery scan (e.g., 'model')") + public String source; /** * Entry *

@@ -64,6 +71,7 @@ public int hashCode() { result = ((result* 31)+((this.generation == null)? 0 :this.generation.hashCode())); result = ((result* 31)+((this.phase == null)? 0 :this.phase.hashCode())); result = ((result* 31)+((this.active_count == null)? 0 :this.active_count.hashCode())); + result = ((result* 31)+((this.source == null)? 0 :this.source.hashCode())); result = ((result* 31)+((this.passive_count == null)? 0 :this.passive_count.hashCode())); result = ((result* 31)+((this.status == null)? 0 :this.status.hashCode())); return result; @@ -78,7 +86,7 @@ public boolean equals(Object other) { return false; } FamilyDiscoveryState rhs = ((FamilyDiscoveryState) other); - return ((((((this.generation == rhs.generation)||((this.generation!= null)&&this.generation.equals(rhs.generation)))&&((this.phase == rhs.phase)||((this.phase!= null)&&this.phase.equals(rhs.phase))))&&((this.active_count == rhs.active_count)||((this.active_count!= null)&&this.active_count.equals(rhs.active_count))))&&((this.passive_count == rhs.passive_count)||((this.passive_count!= null)&&this.passive_count.equals(rhs.passive_count))))&&((this.status == rhs.status)||((this.status!= null)&&this.status.equals(rhs.status)))); + return (((((((this.generation == rhs.generation)||((this.generation!= null)&&this.generation.equals(rhs.generation)))&&((this.phase == rhs.phase)||((this.phase!= null)&&this.phase.equals(rhs.phase))))&&((this.active_count == rhs.active_count)||((this.active_count!= null)&&this.active_count.equals(rhs.active_count))))&&((this.source == rhs.source)||((this.source!= null)&&this.source.equals(rhs.source))))&&((this.passive_count == rhs.passive_count)||((this.passive_count!= null)&&this.passive_count.equals(rhs.passive_count))))&&((this.status == rhs.status)||((this.status!= null)&&this.status.equals(rhs.status)))); } diff --git a/gencode/python/udmi/schema/events_discovery.py b/gencode/python/udmi/schema/events_discovery.py index f96c707683..f7674a90c4 100644 --- a/gencode/python/udmi/schema/events_discovery.py +++ b/gencode/python/udmi/schema/events_discovery.py @@ -37,6 +37,7 @@ class DiscoveryEvents(DataModel): generation: Optional[str] = None status: Optional[Entry] = None family: Optional[Any] = None + source: Optional[str] = None addr: Optional[str] = None network: Optional[str] = None event_no: Optional[int] = None diff --git a/gencode/python/udmi/schema/state_discovery_family.py b/gencode/python/udmi/schema/state_discovery_family.py index 85ccf0d4e9..29228ed4dd 100644 --- a/gencode/python/udmi/schema/state_discovery_family.py +++ b/gencode/python/udmi/schema/state_discovery_family.py @@ -31,4 +31,5 @@ class FamilyDiscoveryState(DataModel): phase: Optional[Phase] = None active_count: Optional[int] = None passive_count: Optional[int] = None + source: Optional[str] = None status: Optional[Entry] = None diff --git a/schema/events_discovery.json b/schema/events_discovery.json index 63ee409bca..169bf8cf8c 100644 --- a/schema/events_discovery.json +++ b/schema/events_discovery.json @@ -29,6 +29,10 @@ "family": { "existingJavaType": "String" }, + "source": { + "description": "Optional source qualifier for the discovery scan (e.g., 'model')", + "type": "string" + }, "addr": { "description": "The primary address of the device (for indicated family)", "type": "string" diff --git a/schema/state_discovery_family.json b/schema/state_discovery_family.json index c6eb4d821b..498532ac29 100644 --- a/schema/state_discovery_family.json +++ b/schema/state_discovery_family.json @@ -29,6 +29,10 @@ "minimum": 0, "type": "integer" }, + "source": { + "description": "Optional source qualifier for the discovery scan (e.g., 'model')", + "type": "string" + }, "status": { "description": "Status information about the discovery operation", "$ref": "file:entry.json" diff --git a/udmis/src/main/java/com/google/bos/udmi/service/core/ModelDiscoveryHandler.java b/udmis/src/main/java/com/google/bos/udmi/service/core/ModelDiscoveryHandler.java index d03cbd5297..fbf3680eec 100644 --- a/udmis/src/main/java/com/google/bos/udmi/service/core/ModelDiscoveryHandler.java +++ b/udmis/src/main/java/com/google/bos/udmi/service/core/ModelDiscoveryHandler.java @@ -1,8 +1,8 @@ package com.google.bos.udmi.service.core; -import static com.google.udmi.util.GeneralUtils.ifNotNullThen; import static com.google.udmi.util.GeneralUtils.isTrue; import static com.google.udmi.util.JsonUtil.fromString; +import static com.google.udmi.util.JsonUtil.isoConvert; import static java.util.Optional.ofNullable; import com.google.bos.udmi.service.access.IotAccessBase; @@ -145,6 +145,7 @@ private Envelope createDiscoveryEnvelope(SubType subType, String deviceId) { envelope.subType = subType; envelope.subFolder = SubFolder.DISCOVERY; envelope.source = MODEL_SOURCE; + envelope.transactionId = isoConvert(generation); return envelope; } @@ -155,6 +156,7 @@ private DiscoveryEvents createDiscoveryEvent(int eventNo) { event.generation = generation; event.event_no = eventNo; event.family = ProtocolFamily.IOT; + event.source = MODEL_SOURCE; return event; } @@ -163,6 +165,7 @@ private void publishState(Phase phase, int activeCount) { familyState.generation = generation; familyState.phase = phase; familyState.active_count = activeCount; + familyState.source = MODEL_SOURCE; DiscoveryState discoveryState = new DiscoveryState(); discoveryState.version = UdmiServicePod.UDMI_VERSION; @@ -171,13 +174,13 @@ private void publishState(Phase phase, int activeCount) { discoveryState.families = new HashMap<>(); discoveryState.families.put(ProtocolFamily.IOT, familyState); - emitMessage(createDiscoveryEnvelope(SubType.STATE, null), discoveryState); + processor.publish(createDiscoveryEnvelope(SubType.STATE, null), discoveryState); } private void publishStartEvent(int activeCount) { DiscoveryEvents countEvent = createDiscoveryEvent(0); countEvent.active_count = activeCount; - emitMessage(createDiscoveryEnvelope(SubType.EVENTS, null), countEvent); + processor.publish(createDiscoveryEnvelope(SubType.EVENTS, null), countEvent); } private void publishDeviceEvent(String deviceId, int eventNo) { @@ -192,16 +195,6 @@ private void publishDeviceEvent(String deviceId, int eventNo) { ? fromString(Metadata.class, metadataStr) : null; - emitMessage(createDiscoveryEnvelope(SubType.EVENTS, deviceId), deviceEvent); - } - - private void emitMessage(Envelope envelope, Object payload) { - if (processor instanceof UufiProcessor uufiProcessor) { - uufiProcessor.publishModelDiscovery(envelope, payload); - return; - } - ifNotNullThen(UdmiServicePod.maybeGetComponent(UufiProcessor.class), - uufi -> uufi.publishModelDiscovery(envelope, payload)); - processor.publish(envelope, payload); + processor.publish(createDiscoveryEnvelope(SubType.EVENTS, deviceId), deviceEvent); } } diff --git a/udmis/src/main/java/com/google/bos/udmi/service/core/TargetProcessor.java b/udmis/src/main/java/com/google/bos/udmi/service/core/TargetProcessor.java index 9891556ea4..40dc173354 100644 --- a/udmis/src/main/java/com/google/bos/udmi/service/core/TargetProcessor.java +++ b/udmis/src/main/java/com/google/bos/udmi/service/core/TargetProcessor.java @@ -14,6 +14,7 @@ import java.util.Date; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import udmi.schema.DiscoveryEvents; import udmi.schema.EndpointConfiguration; import udmi.schema.Envelope; import udmi.schema.Envelope.SubFolder; @@ -46,7 +47,9 @@ protected void defaultHandler(Object defaultedMessage) { publish(defaultedMessage); - if (ModelDiscoveryHandler.MODEL_SOURCE.equals(envelope.source)) { + if (ModelDiscoveryHandler.MODEL_SOURCE.equals(envelope.source) + || (defaultedMessage instanceof DiscoveryEvents de + && ModelDiscoveryHandler.MODEL_SOURCE.equals(de.source))) { debug("Skipping reflector forwarding for model discovery message"); return; } diff --git a/udmis/src/main/java/com/google/bos/udmi/service/core/UufiProcessor.java b/udmis/src/main/java/com/google/bos/udmi/service/core/UufiProcessor.java index e4ce946739..5486859ad0 100644 --- a/udmis/src/main/java/com/google/bos/udmi/service/core/UufiProcessor.java +++ b/udmis/src/main/java/com/google/bos/udmi/service/core/UufiProcessor.java @@ -55,12 +55,8 @@ protected void defaultHandler(Object message) { // Message from MQTT (Client or Loopback) // Loopback protection using source attribute - boolean isModelLoopback = (ModelDiscoveryHandler.MODEL_SOURCE.equals(envelope.source) - || ModelDiscoveryHandler.MODEL_SOURCE.equals(messageMap.get("source"))) - && envelope.subType != SubType.CONFIG; - if ("udmis".equals(envelope.source) || "udmis".equals(messageMap.get("source")) - || isModelLoopback) { - debug("Ignoring loopback of message from %s source", envelope.source); + if ("udmis".equals(envelope.source) || "udmis".equals(messageMap.get("source"))) { + debug("Ignoring loopback of message from udmis source"); return; } @@ -190,10 +186,6 @@ private void handleUufiInbound(Envelope envelope, Map messageMap publish(innerEnvelope, innerPayload); } - void publishModelDiscovery(Envelope envelope, Object message) { - handleUufiOutbound(envelope, message); - } - private void handleUufiOutbound(Envelope envelope, Object message) { boolean isStateUpdate = envelope.subType == SubType.STATE && (envelope.subFolder == null || envelope.subFolder == SubFolder.UPDATE); @@ -248,13 +240,10 @@ private void handleUufiOutbound(Envelope envelope, Object message) { outboundMessage = payloadMap; } - String source = ModelDiscoveryHandler.MODEL_SOURCE.equals(envelope.source) - ? ModelDiscoveryHandler.MODEL_SOURCE : "udmis"; - // Wrap system message for UUFI clients Map uufiMessage = toMap(envelope); uufiMessage.put(PAYLOAD_KEY, outboundMessage); - uufiMessage.put("source", source); // For loopback protection + uufiMessage.put("source", "udmis"); // For loopback protection if (envelope.principal != null) { uufiMessage.put("principal", envelope.principal); } else { @@ -266,7 +255,7 @@ private void handleUufiOutbound(Envelope envelope, Object message) { uufiEnvelope.subFolder = envelope.subFolder; uufiEnvelope.deviceRegistryId = envelope.deviceRegistryId; uufiEnvelope.deviceId = envelope.deviceId; - uufiEnvelope.source = source; + uufiEnvelope.source = "udmis"; // Set udmis source uufiEnvelope.principal = envelope.principal; uufiEnvelope.gatewayId = "uufi"; diff --git a/udmis/src/main/java/com/google/bos/udmi/service/messaging/impl/SimpleMqttPipe.java b/udmis/src/main/java/com/google/bos/udmi/service/messaging/impl/SimpleMqttPipe.java index a1f37fb2e8..dd9789d7bf 100644 --- a/udmis/src/main/java/com/google/bos/udmi/service/messaging/impl/SimpleMqttPipe.java +++ b/udmis/src/main/java/com/google/bos/udmi/service/messaging/impl/SimpleMqttPipe.java @@ -387,7 +387,9 @@ private String makeTopic(Envelope envelope) { if (envelope.subType != null || !topic.isEmpty()) { topic = "/" + envelope.subType + topic; } - String channel = IMPLICIT_CHANNEL.equals(envelope.source) ? "" : sendTopicChannel; + boolean isTargetChannel = IMPLICIT_CHANNEL.equals(envelope.source) + || "model".equals(envelope.source); + String channel = isTargetChannel ? "" : sendTopicChannel; return format("/r/%s/d/%s%s%s", envelope.deviceRegistryId, envelope.deviceId, channel, topic); } diff --git a/udmis/src/test/java/com/google/bos/udmi/service/core/ReflectProcessorTest.java b/udmis/src/test/java/com/google/bos/udmi/service/core/ReflectProcessorTest.java index a58a494dcc..d0605e54fb 100644 --- a/udmis/src/test/java/com/google/bos/udmi/service/core/ReflectProcessorTest.java +++ b/udmis/src/test/java/com/google/bos/udmi/service/core/ReflectProcessorTest.java @@ -198,6 +198,13 @@ public void modelRegistryUpdateTest() { eq(requestModel)); verify(provider, times(1)).listDevices(eq(TEST_REGISTRY), any()); verify(provider, times(1)).fetchDevice(eq(TEST_REGISTRY), eq("AHU-1")); + + // Verify 1 SiteMetadataUpdate + 4 Model Discovery messages emitted to udmi_target: + // 0: SiteMetadataUpdate, 1: start state, 2: event_no=0 count, 3: AHU-1 event, 4: stop state + assertEquals(5, captured.size(), "Expected SiteMetadataUpdate + 4 model discovery messages"); + udmi.schema.DiscoveryEvents devEvent = (udmi.schema.DiscoveryEvents) captured.get(3); + assertEquals("AHU-1", devEvent.addr); + assertEquals("SN-AHU-1", devEvent.model.system.serial_no); } @Test diff --git a/udmis/src/test/java/com/google/bos/udmi/service/core/UufiProcessorTest.java b/udmis/src/test/java/com/google/bos/udmi/service/core/UufiProcessorTest.java index d5f48d2190..e4a8614e0e 100644 --- a/udmis/src/test/java/com/google/bos/udmi/service/core/UufiProcessorTest.java +++ b/udmis/src/test/java/com/google/bos/udmi/service/core/UufiProcessorTest.java @@ -373,7 +373,7 @@ public void modelDiscoveryTest() { activeTestInstance(() -> getReverseDispatcher().publish( new Bundle(transportEnvelope, uufiWrapper))); - // 4 messages published by ModelDiscoveryHandler: + // 5 messages published by ModelDiscoveryHandler to udmi_target: // 1. DiscoveryState (active, active_count=0) // 2. DiscoveryEvents (event_no=0, active_count=2) // 3. DiscoveryEvents (event_no=1, addr=AHU-1, model=...) @@ -381,46 +381,42 @@ public void modelDiscoveryTest() { // 5. DiscoveryState (stopped, active_count=2) assertEquals(5, captured.size(), "Expected 5 model discovery messages"); - Map msg0 = toMap(captured.get(0)); - assertEquals("model", msg0.get("source")); udmi.schema.DiscoveryState startState = - convertTo(udmi.schema.DiscoveryState.class, msg0.get("payload")); + convertTo(udmi.schema.DiscoveryState.class, captured.get(0)); assertEquals(udmi.schema.FamilyDiscoveryState.Phase.ACTIVE, startState.families.get("iot").phase); assertEquals(0, startState.families.get("iot").active_count); + assertEquals("model", startState.families.get("iot").source); - Map msg1 = toMap(captured.get(1)); - assertEquals("model", msg1.get("source")); udmi.schema.DiscoveryEvents startEvent = - convertTo(udmi.schema.DiscoveryEvents.class, msg1.get("payload")); + convertTo(udmi.schema.DiscoveryEvents.class, captured.get(1)); assertEquals(0, startEvent.event_no); assertEquals(2, startEvent.active_count); assertEquals("iot", startEvent.family); + assertEquals("model", startEvent.source); - Map msg2 = toMap(captured.get(2)); - assertEquals("AHU-1", msg2.get("deviceId")); udmi.schema.DiscoveryEvents devEvent1 = - convertTo(udmi.schema.DiscoveryEvents.class, msg2.get("payload")); + convertTo(udmi.schema.DiscoveryEvents.class, captured.get(2)); assertEquals(1, devEvent1.event_no); assertEquals("AHU-1", devEvent1.addr); + assertEquals("model", devEvent1.source); assertNotNull(devEvent1.model); assertEquals("SN-AHU-1", devEvent1.model.system.serial_no); - Map msg3 = toMap(captured.get(3)); - assertEquals("GAT-123", msg3.get("deviceId")); udmi.schema.DiscoveryEvents devEvent2 = - convertTo(udmi.schema.DiscoveryEvents.class, msg3.get("payload")); + convertTo(udmi.schema.DiscoveryEvents.class, captured.get(3)); assertEquals(2, devEvent2.event_no); assertEquals("GAT-123", devEvent2.addr); + assertEquals("model", devEvent2.source); assertNotNull(devEvent2.model); assertEquals("SN-GAT-123", devEvent2.model.system.serial_no); - Map msg4 = toMap(captured.get(4)); udmi.schema.DiscoveryState stopState = - convertTo(udmi.schema.DiscoveryState.class, msg4.get("payload")); + convertTo(udmi.schema.DiscoveryState.class, captured.get(4)); assertEquals(udmi.schema.FamilyDiscoveryState.Phase.STOPPED, stopState.families.get("iot").phase); assertEquals(2, stopState.families.get("iot").active_count); + assertEquals("model", stopState.families.get("iot").source); } /** @@ -465,9 +461,8 @@ public void modelDiscoveryEntriesAndAddrsTest() { // With depth=entries, only 3 messages: start state, event_no=0 count, stop state assertEquals(3, captured.size(), "Expected 3 messages for depth=entries"); - Map msg1 = toMap(captured.get(1)); udmi.schema.DiscoveryEvents startEvent = - convertTo(udmi.schema.DiscoveryEvents.class, msg1.get("payload")); + convertTo(udmi.schema.DiscoveryEvents.class, captured.get(1)); assertEquals(0, startEvent.event_no); assertEquals(1, startEvent.active_count); } From ca64d2b8504190944ecd6061e2950da931f693be Mon Sep 17 00:00:00 2001 From: Khyati Mahendru Date: Mon, 19 Oct 2026 09:40:20 +0000 Subject: [PATCH 3/7] chore(butler): revert psql -w flag in bin/test_butler --- bin/test_butler | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bin/test_butler b/bin/test_butler index 4342e8d1ab..0a58c98b54 100755 --- a/bin/test_butler +++ b/bin/test_butler @@ -24,14 +24,14 @@ PG_DB=${POSTGRES_DB:-postgres} INF_PORT=${INFLUX_PORT:-8086} # Check if PostgreSQL and InfluxDB are running -if psql -w -h 127.0.0.1 -p "$PG_PORT" -U "$PG_USER" -d "$PG_DB" -c "SELECT 1;" >/dev/null 2>&1; then +if psql -h 127.0.0.1 -p "$PG_PORT" -U "$PG_USER" -d "$PG_DB" -c "SELECT 1;" >/dev/null 2>&1; then echo "PostgreSQL is active on port $PG_PORT." echo "Piping simple trace into Butler..." bin/pull_files tests/traces/simple | butler/bin/butler --stdin --always-save-raw echo "Verifying PostgreSQL records in udmi_messages..." - msg_count=$(psql -w -h 127.0.0.1 -p "$PG_PORT" -U "$PG_USER" -d "$PG_DB" -t -A -c "SELECT count(*) FROM udmi_messages;" 2>/dev/null || echo 0) + msg_count=$(psql -h 127.0.0.1 -p "$PG_PORT" -U "$PG_USER" -d "$PG_DB" -t -A -c "SELECT count(*) FROM udmi_messages;" 2>/dev/null || echo 0) echo "udmi_messages count: $msg_count" if [[ $msg_count -gt 0 ]]; then @@ -42,10 +42,10 @@ if psql -w -h 127.0.0.1 -p "$PG_PORT" -U "$PG_USER" -d "$PG_DB" -c "SELECT 1;" > fi echo "Verifying PostgreSQL records in specialized tables..." - sys_count=$(psql -w -h 127.0.0.1 -p "$PG_PORT" -U "$PG_USER" -d "$PG_DB" -t -A -c "SELECT count(*) FROM udmi_system_state;" 2>/dev/null || echo 0) + sys_count=$(psql -h 127.0.0.1 -p "$PG_PORT" -U "$PG_USER" -d "$PG_DB" -t -A -c "SELECT count(*) FROM udmi_system_state;" 2>/dev/null || echo 0) echo "udmi_system_state count: $sys_count" - pt_count=$(psql -w -h 127.0.0.1 -p "$PG_PORT" -U "$PG_USER" -d "$PG_DB" -t -A -c "SELECT count(*) FROM udmi_point_state;" 2>/dev/null || echo 0) + pt_count=$(psql -h 127.0.0.1 -p "$PG_PORT" -U "$PG_USER" -d "$PG_DB" -t -A -c "SELECT count(*) FROM udmi_point_state;" 2>/dev/null || echo 0) echo "udmi_point_state count: $pt_count" else echo "PostgreSQL is not running locally; skipping live database verification (tested via unit tests with mocks)." From ef69f3584f8c6fa478195637df7e71798c5d2cef Mon Sep 17 00:00:00 2001 From: Khyati Mahendru Date: Tue, 20 Oct 2026 07:49:40 +0000 Subject: [PATCH 4/7] Fix nondeterministic message ordering in model discovery tests --- bin/test_uufi_client | 16 +++-- .../service/core/ReflectProcessorTest.java | 9 ++- .../udmi/service/core/UufiProcessorTest.java | 72 ++++++++++++------- 3 files changed, 61 insertions(+), 36 deletions(-) diff --git a/bin/test_uufi_client b/bin/test_uufi_client index e98252e070..2fe0945f70 100755 --- a/bin/test_uufi_client +++ b/bin/test_uufi_client @@ -202,12 +202,16 @@ DISC_LOG_FILE=$(mktemp) DISC_GEN=$(date -u +%Y-%m-%dT%H:%M:%SZ) echo "Subscribing to /r/$REGISTRY_ID/d/+/+/discovery for Model Discovery sequence (gen: $DISC_GEN)..." mosquitto_sub $MQTT_ARGS -i "${CLIENT_ID}_disc_sub" -t "/r/$REGISTRY_ID/d/+/+/discovery" -F "%t %p" | while read -r topic payload; do - echo "$topic $payload" >> "$DISC_LOG_FILE" - PHASE=$(echo "$payload" | jq -r .families.iot.phase 2>/dev/null) - GEN=$(echo "$payload" | jq -r .families.iot.generation 2>/dev/null) - if [[ "$topic" == *"/state/discovery" && "$PHASE" == "stopped" && "$GEN" == "$DISC_GEN" ]]; then - kill -USR1 $$ 2>/dev/null || true - exit 0 + GEN=$(echo "$payload" | jq -r '.generation // .families.iot.generation' 2>/dev/null) + if [[ "$GEN" == "$DISC_GEN" ]]; then + echo "$topic $payload" >> "$DISC_LOG_FILE" + if grep -q '"phase":"active"' "$DISC_LOG_FILE" && \ + grep -q '"event_no":0' "$DISC_LOG_FILE" && \ + grep -q "/r/$REGISTRY_ID/d/$DUT_ID/events/discovery" "$DISC_LOG_FILE" && \ + grep -q '"phase":"stopped"' "$DISC_LOG_FILE"; then + kill -USR1 $$ 2>/dev/null || true + exit 0 + fi fi done & SUB_DISC_PID=$! diff --git a/udmis/src/test/java/com/google/bos/udmi/service/core/ReflectProcessorTest.java b/udmis/src/test/java/com/google/bos/udmi/service/core/ReflectProcessorTest.java index d0605e54fb..902f04ea36 100644 --- a/udmis/src/test/java/com/google/bos/udmi/service/core/ReflectProcessorTest.java +++ b/udmis/src/test/java/com/google/bos/udmi/service/core/ReflectProcessorTest.java @@ -199,10 +199,13 @@ public void modelRegistryUpdateTest() { verify(provider, times(1)).listDevices(eq(TEST_REGISTRY), any()); verify(provider, times(1)).fetchDevice(eq(TEST_REGISTRY), eq("AHU-1")); - // Verify 1 SiteMetadataUpdate + 4 Model Discovery messages emitted to udmi_target: - // 0: SiteMetadataUpdate, 1: start state, 2: event_no=0 count, 3: AHU-1 event, 4: stop state + // Verify 1 SiteMetadataUpdate + 4 Model Discovery messages emitted to udmi_target assertEquals(5, captured.size(), "Expected SiteMetadataUpdate + 4 model discovery messages"); - udmi.schema.DiscoveryEvents devEvent = (udmi.schema.DiscoveryEvents) captured.get(3); + udmi.schema.DiscoveryEvents devEvent = captured.stream() + .filter(o -> o instanceof udmi.schema.DiscoveryEvents de && "AHU-1".equals(de.addr)) + .map(o -> (udmi.schema.DiscoveryEvents) o) + .findFirst() + .orElseThrow(() -> new AssertionError("Missing AHU-1 DiscoveryEvents")); assertEquals("AHU-1", devEvent.addr); assertEquals("SN-AHU-1", devEvent.model.system.serial_no); } diff --git a/udmis/src/test/java/com/google/bos/udmi/service/core/UufiProcessorTest.java b/udmis/src/test/java/com/google/bos/udmi/service/core/UufiProcessorTest.java index e4a8614e0e..722c54352b 100644 --- a/udmis/src/test/java/com/google/bos/udmi/service/core/UufiProcessorTest.java +++ b/udmis/src/test/java/com/google/bos/udmi/service/core/UufiProcessorTest.java @@ -374,49 +374,64 @@ public void modelDiscoveryTest() { new Bundle(transportEnvelope, uufiWrapper))); // 5 messages published by ModelDiscoveryHandler to udmi_target: - // 1. DiscoveryState (active, active_count=0) - // 2. DiscoveryEvents (event_no=0, active_count=2) - // 3. DiscoveryEvents (event_no=1, addr=AHU-1, model=...) - // 4. DiscoveryEvents (event_no=2, addr=GAT-123, model=...) - // 5. DiscoveryState (stopped, active_count=2) + // - DiscoveryState (active, active_count=0) + // - DiscoveryEvents (event_no=0, active_count=2) + // - DiscoveryEvents (event_no=1, addr=AHU-1, model=...) + // - DiscoveryEvents (event_no=2, addr=GAT-123, model=...) + // - DiscoveryState (stopped, active_count=2) assertEquals(5, captured.size(), "Expected 5 model discovery messages"); - udmi.schema.DiscoveryState startState = - convertTo(udmi.schema.DiscoveryState.class, captured.get(0)); - assertEquals(udmi.schema.FamilyDiscoveryState.Phase.ACTIVE, - startState.families.get("iot").phase); + List states = captured.stream() + .filter(o -> o instanceof udmi.schema.DiscoveryState) + .map(o -> (udmi.schema.DiscoveryState) o) + .toList(); + assertEquals(2, states.size(), "Expected 2 DiscoveryState bookend messages"); + + udmi.schema.DiscoveryState startState = states.stream() + .filter(s -> s.families.get("iot").phase == udmi.schema.FamilyDiscoveryState.Phase.ACTIVE) + .findFirst() + .orElseThrow(() -> new AssertionError("Missing ACTIVE DiscoveryState")); assertEquals(0, startState.families.get("iot").active_count); assertEquals("model", startState.families.get("iot").source); - udmi.schema.DiscoveryEvents startEvent = - convertTo(udmi.schema.DiscoveryEvents.class, captured.get(1)); - assertEquals(0, startEvent.event_no); + udmi.schema.DiscoveryState stopState = states.stream() + .filter(s -> s.families.get("iot").phase == udmi.schema.FamilyDiscoveryState.Phase.STOPPED) + .findFirst() + .orElseThrow(() -> new AssertionError("Missing STOPPED DiscoveryState")); + assertEquals(2, stopState.families.get("iot").active_count); + assertEquals("model", stopState.families.get("iot").source); + + List events = captured.stream() + .filter(o -> o instanceof udmi.schema.DiscoveryEvents) + .map(o -> (udmi.schema.DiscoveryEvents) o) + .toList(); + assertEquals(3, events.size(), "Expected 3 DiscoveryEvents messages"); + + udmi.schema.DiscoveryEvents startEvent = events.stream() + .filter(e -> Integer.valueOf(0).equals(e.event_no)) + .findFirst() + .orElseThrow(() -> new AssertionError("Missing event_no=0 DiscoveryEvents")); assertEquals(2, startEvent.active_count); assertEquals("iot", startEvent.family); assertEquals("model", startEvent.source); - udmi.schema.DiscoveryEvents devEvent1 = - convertTo(udmi.schema.DiscoveryEvents.class, captured.get(2)); - assertEquals(1, devEvent1.event_no); + udmi.schema.DiscoveryEvents devEvent1 = events.stream() + .filter(e -> Integer.valueOf(1).equals(e.event_no)) + .findFirst() + .orElseThrow(() -> new AssertionError("Missing event_no=1 DiscoveryEvents")); assertEquals("AHU-1", devEvent1.addr); assertEquals("model", devEvent1.source); assertNotNull(devEvent1.model); assertEquals("SN-AHU-1", devEvent1.model.system.serial_no); - udmi.schema.DiscoveryEvents devEvent2 = - convertTo(udmi.schema.DiscoveryEvents.class, captured.get(3)); - assertEquals(2, devEvent2.event_no); + udmi.schema.DiscoveryEvents devEvent2 = events.stream() + .filter(e -> Integer.valueOf(2).equals(e.event_no)) + .findFirst() + .orElseThrow(() -> new AssertionError("Missing event_no=2 DiscoveryEvents")); assertEquals("GAT-123", devEvent2.addr); assertEquals("model", devEvent2.source); assertNotNull(devEvent2.model); assertEquals("SN-GAT-123", devEvent2.model.system.serial_no); - - udmi.schema.DiscoveryState stopState = - convertTo(udmi.schema.DiscoveryState.class, captured.get(4)); - assertEquals(udmi.schema.FamilyDiscoveryState.Phase.STOPPED, - stopState.families.get("iot").phase); - assertEquals(2, stopState.families.get("iot").active_count); - assertEquals("model", stopState.families.get("iot").source); } /** @@ -461,8 +476,11 @@ public void modelDiscoveryEntriesAndAddrsTest() { // With depth=entries, only 3 messages: start state, event_no=0 count, stop state assertEquals(3, captured.size(), "Expected 3 messages for depth=entries"); - udmi.schema.DiscoveryEvents startEvent = - convertTo(udmi.schema.DiscoveryEvents.class, captured.get(1)); + udmi.schema.DiscoveryEvents startEvent = captured.stream() + .filter(o -> o instanceof udmi.schema.DiscoveryEvents) + .map(o -> (udmi.schema.DiscoveryEvents) o) + .findFirst() + .orElseThrow(() -> new AssertionError("Missing DiscoveryEvents for depth=entries")); assertEquals(0, startEvent.event_no); assertEquals(1, startEvent.active_count); } From 9e881a1b600dc99ab95b916de9bff8a8a0b2186b Mon Sep 17 00:00:00 2001 From: Khyati Mahendru Date: Thu, 22 Oct 2026 13:09:44 +0000 Subject: [PATCH 5/7] feat(discovery): implement Site, Shard (addrs), and Closure resolution for model discovery --- .gencode_hash.txt | 10 +- bin/test_butler | 9 +- bin/test_uufi_client | 60 +++- butler/src/discovery.py | 4 +- butler/src/handlers/discovery.py | 76 ++++- butler/tests/test_handlers.py | 52 ++++ docs/specs/discovery.md | 21 +- gencode/docs/events.html | 88 +++++- gencode/docs/events_discovery.html | 184 +++++++++++- gencode/docs/metadata.html | 102 ++++++- gencode/java/udmi/schema/DiscoveryEvents.java | 11 +- .../python/udmi/schema/events_discovery.py | 3 +- mcp/butler/provider.py | 32 +- schema/events_discovery.json | 7 + schema/model_pointset_point.json | 4 +- .../model_discovery_start.json | 2 + .../service/core/ModelDiscoveryHandler.java | 278 +++++++++++++++--- .../udmi/service/core/ReflectProcessor.java | 8 +- .../service/core/ReflectProcessorTest.java | 51 ++++ .../udmi/service/core/UufiProcessorTest.java | 193 ++++++++++++ .../google/daq/mqtt/registrar/Registrar.java | 37 ++- .../google/daq/mqtt/util/CloudIotManager.java | 13 + .../daq/mqtt/registrar/RegistrarTest.java | 17 ++ 23 files changed, 1179 insertions(+), 83 deletions(-) diff --git a/.gencode_hash.txt b/.gencode_hash.txt index cf376b1f3b..0871df9a2b 100644 --- a/.gencode_hash.txt +++ b/.gencode_hash.txt @@ -7,15 +7,15 @@ c21266e8de23b6e8acfac16a177db782375bf71df470c343c3f8eb8b822ef56a gencode/docs/c 94bb566ac6a4b6b7f89cd2cf1911f658446cc0bea405014275104623f9964dd3 gencode/docs/configuration_pubber.html 1057fa40fb7a31a23bb2773d21c38cf4590a935bd8b5ea4218e695c6204f5dd9 gencode/docs/data_template.html 1d5294b4a1fea6555e23144e594523e4793c795069b6880ce607ad09a6015581 gencode/docs/equipment_translation.html -ad23d7fa3cf403b35565c7058f68c41f02c3e7c10614e95dcafa69f53b1e2c08 gencode/docs/events.html +cbcf08f38e9601db5e476ca0f47a54eb25f30f78717cd328b1fcc15b67c26891 gencode/docs/events.html 70e57ad6ef39330d958727ebf9dcd61ef6ea30e4c8653eac412bf1867fdb3a70 gencode/docs/events_alarmset.html -03b1c67910c9244476ab62e11ebc3c2c8c164e3b4f289f96446b1a3a24f62373 gencode/docs/events_discovery.html +a69403cdca757a198785bb52766c1601e76ed5e52ea0ea99c3ca572740ea2531 gencode/docs/events_discovery.html eafcc3c48189f605f114cde051fea9d13fc5f1a3e395d64fb0a91cb53d4c9aeb gencode/docs/events_pointset.html b65ce6e8061e61ba676a72c624f1c425bd238e152ce8e3ebb1e9c23f0e502bcb gencode/docs/events_streams.html 81043ba732caab75704d38e6977b32b1c2bcdad89d94ad2a259d0e70ca4aa40e gencode/docs/events_system.html 46fd37b577c655965346d1e3ececf559c4384791214900eeeea1ad8fc72a02b8 gencode/docs/events_udmi.html baf59590b1a71a49e59d8f499ae5112a1b7a555294dbd72bc821203aecceefa3 gencode/docs/events_validation.html -5141c93cffcd2e00e47538b8a0f1fc11eaa7706756e8ccdd1b4c803b8167b4ef gencode/docs/metadata.html +c1f67617826bb9ce98ced54a512d7d45939733ff87cc9b5449484322dde13e1e gencode/docs/metadata.html 18178a591940c176371fb9196048eaacd40f1323997cf907ca11eb879a095c7e gencode/docs/monitoring.html 64d21cf916e20566171cf9d723e59c297b576bcba89e2a73aec2b018398cc7da gencode/docs/mosquitto_client_response.html 6f332da9cc64230df3551f9748a4eaa4060146e767cae967fc0e73b1f348be5b gencode/docs/persistent_device.html @@ -66,7 +66,7 @@ c04302f973508295258d4d3a29b5da70bf3cd3a6e2c63a958fb70dd3d370276c gencode/java/u 52ae2afb2dcb7863259e7eea71888811663c010429c09760493f5f006a660c84 gencode/java/udmi/schema/Dimension.java 2bb1513817eea9c0486b28f3316ead16d53885a8bad9f6a42ee3d5f762335a68 gencode/java/udmi/schema/DiscoveryCommand.java a572f17a61584a32ff19968356f7f7d7bcc8430131ac3701878d0bd221e1a1fa gencode/java/udmi/schema/DiscoveryConfig.java -125646ad6cd48e61b743ed0342283b42efba15f072bbdb882c6d9871930154fd gencode/java/udmi/schema/DiscoveryEvents.java +5d93ed972e510262af1384c1b6ad134215569aabb84d77694ec46a8a619abf66 gencode/java/udmi/schema/DiscoveryEvents.java af16026c143a592562b77012dd739c02364bd1a9d6bbd6c7ede4f3a5a38b7f4a gencode/java/udmi/schema/DiscoveryModel.java 0fd348086bfd8e38a54a06b0fd205f6d01fdc6236f07ccc8bef49a1832f8c767 gencode/java/udmi/schema/DiscoveryState.java baef666f5f58f790b73ba7bed8f60eacdd75ba3151b9abc32265954a9da84faa gencode/java/udmi/schema/Electricity.java @@ -203,7 +203,7 @@ ab1a8ee1f12d04dd88837b6f026030b5ed3550ef3bfbbfbcb4f77cc4df39e93a gencode/python 3dcf4daa94b8450b7360e58500e20e1fbe5701502ac28358112ee1a6bd2a87f1 gencode/python/udmi/schema/events.py af48a2d32b4ec326df6e3f165d8944b994fb426bd4e663c42c1d592f5cb41f83 gencode/python/udmi/schema/events_alarmset.py b58371ffa7979d1598502a35859f3010ae7a2ec2d851454c010c835e426c7a14 gencode/python/udmi/schema/events_alarmset_alarm.py -46ed7eb98290eee6a2a3b271be75269b6dc2d5a439c61150384c2f1efe9f68bb gencode/python/udmi/schema/events_discovery.py +584e3482138fb2210043bf6b7ca553455391ed678b4988406e247e348a7834d9 gencode/python/udmi/schema/events_discovery.py 84700f9d8a8975130ff9c62891003f7a12f367fa7dd1ca1db5f5fae7225bf005 gencode/python/udmi/schema/events_pointset.py 9f568bd573d4992e3efa3cc3cb8cdefecb95b15675b0442873abdc7850d108f9 gencode/python/udmi/schema/events_pointset_point.py 282439e4983ae9f310fda69dd6ff979567fa983ebdefcde407df42d631fe1bad gencode/python/udmi/schema/events_streams.py diff --git a/bin/test_butler b/bin/test_butler index 0a58c98b54..db3960b0d1 100755 --- a/bin/test_butler +++ b/bin/test_butler @@ -21,17 +21,18 @@ echo "========================================================================== PG_PORT=${POSTGRES_PORT:-5432} PG_USER=${POSTGRES_USER:-postgres} PG_DB=${POSTGRES_DB:-postgres} +export PGPASSWORD=${POSTGRES_PASSWORD:-postgres} INF_PORT=${INFLUX_PORT:-8086} # Check if PostgreSQL and InfluxDB are running -if psql -h 127.0.0.1 -p "$PG_PORT" -U "$PG_USER" -d "$PG_DB" -c "SELECT 1;" >/dev/null 2>&1; then +if psql -w -h 127.0.0.1 -p "$PG_PORT" -U "$PG_USER" -d "$PG_DB" -c "SELECT 1;" >/dev/null 2>&1; then echo "PostgreSQL is active on port $PG_PORT." echo "Piping simple trace into Butler..." bin/pull_files tests/traces/simple | butler/bin/butler --stdin --always-save-raw echo "Verifying PostgreSQL records in udmi_messages..." - msg_count=$(psql -h 127.0.0.1 -p "$PG_PORT" -U "$PG_USER" -d "$PG_DB" -t -A -c "SELECT count(*) FROM udmi_messages;" 2>/dev/null || echo 0) + msg_count=$(psql -w -h 127.0.0.1 -p "$PG_PORT" -U "$PG_USER" -d "$PG_DB" -t -A -c "SELECT count(*) FROM udmi_messages;" 2>/dev/null || echo 0) echo "udmi_messages count: $msg_count" if [[ $msg_count -gt 0 ]]; then @@ -42,10 +43,10 @@ if psql -h 127.0.0.1 -p "$PG_PORT" -U "$PG_USER" -d "$PG_DB" -c "SELECT 1;" >/de fi echo "Verifying PostgreSQL records in specialized tables..." - sys_count=$(psql -h 127.0.0.1 -p "$PG_PORT" -U "$PG_USER" -d "$PG_DB" -t -A -c "SELECT count(*) FROM udmi_system_state;" 2>/dev/null || echo 0) + sys_count=$(psql -w -h 127.0.0.1 -p "$PG_PORT" -U "$PG_USER" -d "$PG_DB" -t -A -c "SELECT count(*) FROM udmi_system_state;" 2>/dev/null || echo 0) echo "udmi_system_state count: $sys_count" - pt_count=$(psql -h 127.0.0.1 -p "$PG_PORT" -U "$PG_USER" -d "$PG_DB" -t -A -c "SELECT count(*) FROM udmi_point_state;" 2>/dev/null || echo 0) + pt_count=$(psql -w -h 127.0.0.1 -p "$PG_PORT" -U "$PG_USER" -d "$PG_DB" -t -A -c "SELECT count(*) FROM udmi_point_state;" 2>/dev/null || echo 0) echo "udmi_point_state count: $pt_count" else echo "PostgreSQL is not running locally; skipping live database verification (tested via unit tests with mocks)." diff --git a/bin/test_uufi_client b/bin/test_uufi_client index 2fe0945f70..162cad48bd 100755 --- a/bin/test_uufi_client +++ b/bin/test_uufi_client @@ -247,5 +247,63 @@ grep -q '"source":"model"' "$DISC_LOG_FILE" || { echo "ERROR: Missing source:mod grep -q '"phase":"stopped"' "$DISC_LOG_FILE" || { echo "ERROR: Missing stopped DiscoveryState in Model Discovery" >&2; cat "$DISC_LOG_FILE"; exit 1; } echo "Site Model Discovery SUCCESS: Verified start state, event_no=0 count, device model events, and stop state" +# 5. Partial (Shard) Site Model Discovery & Closure Expansion Test +echo "Starting Partial (Shard) Site Model Discovery & Closure test..." +SHARD_LOG_FILE=$(mktemp) +SHARD_GEN=$(date -u -d "+1 second" +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u +%Y-%m-%dT%H:%M:%SZ) +if [[ "$SHARD_GEN" == "$DISC_GEN" ]]; then + sleep 1 + SHARD_GEN=$(date -u +%Y-%m-%dT%H:%M:%SZ) +fi +echo "Subscribing to /r/$REGISTRY_ID/d/+/+/discovery for Sharded Model Discovery (gen: $SHARD_GEN, shard: [AHU-22])..." +mosquitto_sub $MQTT_ARGS -i "${CLIENT_ID}_shard_sub" -t "/r/$REGISTRY_ID/d/+/+/discovery" -F "%t %p" | while read -r topic payload; do + GEN=$(echo "$payload" | jq -r '.generation // .families.iot.generation' 2>/dev/null) + if [[ "$GEN" == "$SHARD_GEN" ]]; then + echo "$topic $payload" >> "$SHARD_LOG_FILE" + if grep -q '"phase":"active"' "$SHARD_LOG_FILE" && \ + grep -q '"event_no":0' "$SHARD_LOG_FILE" && \ + grep -q "/r/$REGISTRY_ID/d/AHU-22/events/discovery" "$SHARD_LOG_FILE" && \ + grep -q "/r/$REGISTRY_ID/d/GAT-123/events/discovery" "$SHARD_LOG_FILE" && \ + grep -q "/r/$REGISTRY_ID/d/SNS-4/events/discovery" "$SHARD_LOG_FILE" && \ + grep -q '"phase":"stopped"' "$SHARD_LOG_FILE"; then + kill -USR1 $$ 2>/dev/null || true + exit 0 + fi + fi +done & +SUB_SHARD_PID=$! + +GOT_SHARD=0 +trap "GOT_SHARD=1" USR1 +sleep 2 + +SHARD_CONFIG="{\"subType\":\"config\",\"subFolder\":\"discovery\",\"deviceRegistryId\":\"$REGISTRY_ID\",\"payload\":{\"version\":\"1.5.7\",\"timestamp\":\"$SHARD_GEN\",\"generation\":\"$SHARD_GEN\",\"families\":{\"iot\":{\"generation\":\"$SHARD_GEN\",\"depth\":\"details\",\"source\":\"model\",\"addrs\":[\"AHU-22\"]}}}}" +echo "Publishing Sharded Model Discovery config to /uufi/r/$REGISTRY_ID/c/config/discovery: $SHARD_CONFIG" +mosquitto_pub $MQTT_ARGS -i "${CLIENT_ID}_shard_pub" -t "/uufi/r/$REGISTRY_ID/c/config/discovery" -m "$SHARD_CONFIG" + +TIMEOUT=30 +while [ $GOT_SHARD -eq 0 ] && [ $TIMEOUT -gt 0 ]; do + echo "Waiting for Sharded Model Discovery completion... ($TIMEOUT)" + sleep 1 + TIMEOUT=$((TIMEOUT - 1)) +done + +kill $SUB_SHARD_PID 2>/dev/null || true + +if [ $GOT_SHARD -eq 0 ]; then + echo "ERROR: Timed out waiting for Sharded Model Discovery sequence completion" >&2 + cat "$SHARD_LOG_FILE" || true + exit 1 +fi + +grep -q '"addrs":\["AHU-22"\]' "$SHARD_LOG_FILE" || { echo "ERROR: Missing addrs:[AHU-22] on event_no=0 in Sharded Model Discovery" >&2; cat "$SHARD_LOG_FILE"; exit 1; } +grep -q '"active_count":3' "$SHARD_LOG_FILE" || { echo "ERROR: Expected active_count:3 (Closure of AHU-22 -> GAT-123 -> SNS-4) in Sharded Model Discovery" >&2; cat "$SHARD_LOG_FILE"; exit 1; } +if grep -q "/r/$REGISTRY_ID/d/AHU-1/events/discovery" "$SHARD_LOG_FILE"; then + echo "ERROR: Unrelated device AHU-1 should not be in Closure([AHU-22])" >&2 + cat "$SHARD_LOG_FILE" + exit 1 +fi +echo "Sharded Site Model Discovery SUCCESS: Verified addrs:[AHU-22], active_count:3, and Closure expansion [AHU-22, GAT-123, SNS-4]" + echo "UUFI Comprehensive Integration Test PASSED" -rm -f "$REPLY_FILE" "$STATE_REPLY_FILE" "$MODEL_REPLY_FILE" "$DISC_LOG_FILE" +rm -f "$REPLY_FILE" "$STATE_REPLY_FILE" "$MODEL_REPLY_FILE" "$DISC_LOG_FILE" "$SHARD_LOG_FILE" diff --git a/butler/src/discovery.py b/butler/src/discovery.py index 9392810565..f15a34ed31 100644 --- a/butler/src/discovery.py +++ b/butler/src/discovery.py @@ -10,7 +10,7 @@ from butler.src.connection import ButlerConnection -def run_discovery(conn_spec, registry_id, device_id=None, target_families=None, site_model=None): +def run_discovery(conn_spec, registry_id, device_id=None, target_families=None, site_model=None, addrs=None): """Dispatches discovery configuration message over the configured project spec / broker.""" is_registry_discovery = not device_id actual_device_id = None if is_registry_discovery else device_id @@ -30,6 +30,8 @@ def run_discovery(conn_spec, registry_id, device_id=None, target_families=None, "depth": "details", "source": "model", } + if addrs: + families_dict[f]["addrs"] = list(addrs) else: families_dict[f] = {"generation": now} else: diff --git a/butler/src/handlers/discovery.py b/butler/src/handlers/discovery.py index 6eda9cbdba..33403616f8 100644 --- a/butler/src/handlers/discovery.py +++ b/butler/src/handlers/discovery.py @@ -32,9 +32,58 @@ def get_discovered_family_addr(payload: Dict[str, Any], family: str) -> Optional class DiscoveryHandler(BaseHandler): """Processes discovery events and saves rows into PostgreSQL udmi_discovery table.""" + def __init__(self) -> None: + super().__init__() + self._iot_batches: Dict[tuple, Dict[str, Any]] = {} + def can_handle(self, sub_folder: Optional[str], sub_type: Optional[str], payload: Dict[str, Any]) -> bool: return sub_folder == "discovery" and sub_type in (None, "events") + def _reconcile_iot_batch( + self, + registry_id: Optional[str], + generation: Optional[str], + postgres_manager: PostgresManager, + ) -> None: + if not registry_id or not generation: + return + key = (registry_id, generation) + batch = self._iot_batches.get(key) + if not batch or not batch.get("has_start"): + return + active_count = batch.get("active_count") + seen_events: Dict[int, str] = batch.get("seen_events", {}) + if not isinstance(active_count, int) or len(seen_events) < active_count: + return + + closure_ids = set(seen_events.values()) + addrs = batch.get("addrs") + self._iot_batches.pop(key, None) + + if isinstance(addrs, list): + deleted_ids = sorted(set(str(a) for a in addrs if a) - closure_ids) + if deleted_ids: + placeholders = ", ".join(["%s"] * len(deleted_ids)) + sql = ( + f"DELETE FROM udmi_metadata " + f"WHERE device_registry_id = %s AND device_id IN ({placeholders})" + ) + postgres_manager.execute_sql(sql, tuple([registry_id] + deleted_ids)) + else: + if closure_ids: + sorted_closure = sorted(closure_ids) + placeholders = ", ".join(["%s"] * len(sorted_closure)) + sql = ( + f"DELETE FROM udmi_metadata " + f"WHERE device_registry_id = %s AND device_id NOT IN ({placeholders})" + ) + postgres_manager.execute_sql(sql, tuple([registry_id] + sorted_closure)) + else: + postgres_manager.execute_sql( + "DELETE FROM udmi_metadata WHERE device_registry_id = %s", + (registry_id,), + ) + def process_message( self, envelope: Dict[str, Any], @@ -46,14 +95,28 @@ def process_message( payload = payload["payload"] family = payload.get("family") event_no = payload.get("event_no") + generation = payload.get("generation") + registry_id = envelope.get("deviceRegistryId") model_data = payload.get("model") if isinstance(payload.get("model"), dict) else {} if isinstance(event_no, int) and event_no <= 0 and not payload.get("addr") and not model_data: - return { + res: Dict[str, Any] = { "target": "postgres", "table": "udmi_discovery", "count": 0, } + if payload.get("addrs") is not None: + res["addrs"] = payload.get("addrs") + if family == "iot" and event_no == 0 and registry_id and generation: + batch = self._iot_batches.setdefault( + (registry_id, generation), + {"has_start": False, "active_count": 0, "addrs": None, "seen_events": {}}, + ) + batch["has_start"] = True + batch["active_count"] = payload.get("active_count", 0) + batch["addrs"] = payload.get("addrs") + self._reconcile_iot_batch(registry_id, generation, postgres_manager) + return res ports: List[Dict[str, Any]] = [] if family == "ether" and (refs := payload.get("refs")): @@ -77,8 +140,8 @@ def process_message( row = { "timestamp": payload.get("timestamp") or envelope.get("publishTime"), - "generation": payload.get("generation"), - "device_registry_id": envelope.get("deviceRegistryId"), + "generation": generation, + "device_registry_id": registry_id, "device_id": device_id, "message_id": envelope.get("messageId"), "scan_family": family, @@ -104,6 +167,13 @@ def process_message( if device_id: model_env["deviceId"] = device_id MetadataHandler().process_message(model_env, model_data, postgres_manager, influx_manager) + if registry_id and generation and isinstance(event_no, int) and event_no > 0 and device_id: + batch = self._iot_batches.setdefault( + (registry_id, generation), + {"has_start": False, "active_count": 0, "addrs": None, "seen_events": {}}, + ) + batch["seen_events"][event_no] = device_id + self._reconcile_iot_batch(registry_id, generation, postgres_manager) return { "target": "postgres", diff --git a/butler/tests/test_handlers.py b/butler/tests/test_handlers.py index f135b8a291..d2ac5794f4 100644 --- a/butler/tests/test_handlers.py +++ b/butler/tests/test_handlers.py @@ -189,12 +189,15 @@ def test_model_discovery_handler(self): start_env = {"deviceRegistryId": "reg-1"} start_payload = { "family": "iot", + "source": "model", "generation": "2026-08-21T00:00:00Z", "event_no": 0, + "addrs": ["AHU-1", "DEL-1"], "active_count": 1, } start_res = handler.process_message(start_env, start_payload, self.mock_pg, self.mock_influx) self.assertEqual(start_res["count"], 0) + self.assertEqual(start_res["addrs"], ["AHU-1", "DEL-1"]) self.mock_pg.insert_row.assert_not_called() envelope = {"deviceId": "AHU-1", "deviceRegistryId": "reg-1"} @@ -234,6 +237,55 @@ def test_model_discovery_handler(self): self.assertEqual(meta_table, "udmi_metadata") self.assertEqual(meta_row["device_id"], "AHU-1") self.assertEqual(meta_row["metadata"]["system"]["serial_no"], "SN-AHU-1") + self.mock_pg.execute_sql.assert_called_once_with( + "DELETE FROM udmi_metadata WHERE device_registry_id = %s AND device_id IN (%s)", + ("reg-1", "DEL-1"), + ) + + def test_provider_discovered_models_deletion_reconciliation(self): + from mcp.butler.provider import ButlerProvider + + mock_conn = MagicMock() + mock_cur = MagicMock() + mock_conn.cursor.return_value.__enter__.return_value = mock_cur + mock_cur.fetchall.side_effect = [ + [ + ("AHU-1", {"system": {"serial_no": "SN-OLD"}}), + ("DEL-1", {"system": {"serial_no": "SN-DEL"}}), + ("VAV-1", {"system": {"serial_no": "SN-VAV"}}), + ], + [ + ( + None, + "events", + "discovery", + { + "family": "iot", + "generation": "2026-08-21T01:00:00Z", + "event_no": 0, + "addrs": ["AHU-1", "DEL-1"], + "active_count": 1, + }, + ), + ( + "AHU-1", + "events", + "discovery", + { + "family": "iot", + "generation": "2026-08-21T01:00:00Z", + "event_no": 1, + "addr": "AHU-1", + "model": {"system": {"serial_no": "SN-NEW"}}, + }, + ), + ], + ] + self.mock_pg.get_connection.return_value = mock_conn + provider = ButlerProvider(pg_manager=self.mock_pg, influx_manager=self.mock_influx) + models = provider.get_discovered_models("reg-1") + self.assertEqual(set(models.keys()), {"AHU-1", "VAV-1"}) + self.assertEqual(models["AHU-1"]["system"]["serial_no"], "SN-NEW") def test_registry_model_discovery_topics(self): from butler.src.connection import ButlerConnection diff --git a/docs/specs/discovery.md b/docs/specs/discovery.md index a878ea3d2a..c0ab14f80a 100644 --- a/docs/specs/discovery.md +++ b/docs/specs/discovery.md @@ -84,9 +84,22 @@ be reported: Site Model Discovery exposes the cloud-registered device models of a registry over the standard UDMI discovery pipeline (`family: "iot"`, `source: "model"`). It is triggered automatically when a registry model update is processed by UDMIS, or on-demand by publishing a registry-scoped [`config`](../../gencode/docs/config.html) discovery message with `families.iot.source` set to `"model"`. -The Site Model Discovery sequence is published to the `udmi_target` stream with envelope attributes `source: "model"` and `transactionId` set to the `generation` timestamp, and follows the standard discovery state and event bookends: +To support both full-site snapshots and partial/incremental updates, Site Model Discovery distinguishes three sets of devices: + +* **`Site` (Declared Set):** All active (non-blocked) devices registered in the building (`deviceRegistryId`). +* **`Shard` (Declared Set):** The working set of changed or targeted device IDs (`addrs`), specified via `families.iot.addrs` in `config/discovery` (or via a targeted `registrar` update, including `-T` transitive proxy expansion and `-b` / `-d` / `-x` targeted device blocking/deletion) and echoed on the `event_no = 0` start event (`addrs`). When `addrs` is omitted/null on `event_no = 0`, the scan is a full-site scan (`Shard == Closure == Site`). +* **`Closure` (Calculated Set):** The set of active devices emitted in `event_no = 1..N` (`active_count = |Closure|`), comprising: + 1. All active devices in the `Shard` ($S \cap \text{Site}$), + 2. All active devices in the `Site` that directly reference any device in the `Shard` (including deleted/blocked devices in the `Shard`) via `model.relationships` or `model.gateway` (`proxy_ids` / `gateway_id`, treated bidirectionally so gateway-proxy pairs are always kept together), + 3. All active devices in the `Site` that transitively reference any device in the `Shard` (inbound) via `model.pointset.points.*.expr` (`:`, excluding `$SELF` self-references), and + 4. All active devices in the `Site` transitively referenced (outbound) from the above set across `model.relationships`, `model.pointset.points.*.expr`, and `model.gateway`. + +The Site Model Discovery sequence is serialized per registry and published to the `udmi_target` stream with envelope attributes `source: "model"` and `transactionId` set to the `generation` timestamp: 1. **Start State (`state/discovery`):** Published at the registry level with `families.iot.source: "model"`, `families.iot.phase: "active"`, and `families.iot.active_count: 0`. -2. **Start & Count Event (`events/discovery`):** Published at the registry level with `source: "model"`, `event_no: 0`, `family: "iot"`, and `active_count` set to the total number of matching devices in the registry. -3. **Device Model Events (`events/discovery`):** When `depth` is `details` (or `parts`, or omitted), one event per device (`event_no: 1..N`) is published with `source: "model"`, `family: "iot"`, `addr` set to the device ID, and `model` populated with the device's [`metadata`](../../gencode/docs/metadata.html) object. When `depth` is `entries`, individual device events are omitted and only the `event_no: 0` count is emitted. If `addrs` is specified in `families.iot`, only matching device IDs are included. -4. **Stop State (`state/discovery`):** Published at the registry level with `families.iot.source: "model"`, `families.iot.phase: "stopped"`, and `families.iot.active_count` set to the total number of discovered devices. +2. **Start, Count & Shard Scope Event (`events/discovery`):** Published at the registry level with `source: "model"`, `event_no: 0`, `family: "iot"`, `active_count` set to the size of the `Closure`, and optional `addrs` declaring the `Shard` for partial scans. +3. **Device Model Events (`events/discovery`):** When `depth` is `details` (or `parts`, or omitted), one event per device in the `Closure` (`event_no: 1..N`) is published with `source: "model"`, `family: "iot"`, `addr` set to the device ID, and `model` populated with the device's [`metadata`](../../gencode/docs/metadata.html) object. When `depth` is `entries`, individual device events are omitted and only the `event_no: 0` count is emitted. +4. **Stop State (`state/discovery`):** Published at the registry level with `families.iot.source: "model"`, `families.iot.phase: "stopped"`, and `families.iot.active_count` set to the total number of discovered devices in the `Closure`. If an unrecoverable error occurs during the scan (such as malformed cloud metadata), UDMIS fails fast and publishes a terminal `phase: "stopped"` state containing a `status` error entry (`category: "discovery.family.scan"`, `level: 500`). + +Because `state/discovery` and `events/discovery` may traverse independent transport queues, consumers that subscribe to both streams should treat batch completion as an **unordered barrier** (`event_no = 0` received AND all `1..active_count` device events received AND, if monitoring state, `phase == "stopped"` received) rather than assuming strict message arrival order. + diff --git a/gencode/docs/events.html b/gencode/docs/events.html index 35cdc54037..3b10b44c9c 100644 --- a/gencode/docs/events.html +++ b/gencode/docs/events.html @@ -6832,6 +6832,82 @@

+
+
+
+

+ +

+
+ +
+
+ + Type: array of string
+

List of target addresses for a partial discovery scan (shard)

+
+ + + + + + No Additional Items

Each item of this array must be:

+
+
+ + + Type: string
+ + + + + + + +
+
+
+
+
+
@@ -18033,16 +18109,18 @@

expr

Type: string

Virtual equipment mapping linking this local point to a remote point in another device

-
Must match regular expression: ^[-0-9a-zA-Z$]+:[a-z0-9_]+$ +Must match regular expression: .*[-_0-9a-zA-Z$]+:[a-z0-9_]+.*
-
Example:
+
Examples:

"VAV-3:supply_air_flowrate_sensor"
 
+
"VAV-101:zone_air_temp_sensor - VAV-102:zone_air_temp_sensor"
+
@@ -51523,16 +51601,18 @@

exprType: string

Virtual equipment mapping linking this local point to a remote point in another device

-
Must match regular expression: ^[-0-9a-zA-Z$]+:[a-z0-9_]+$ +Must match regular expression: .*[-_0-9a-zA-Z$]+:[a-z0-9_]+.*
-
Example:
+
Examples:

"VAV-3:supply_air_flowrate_sensor"
 
+
"VAV-101:zone_air_temp_sensor - VAV-102:zone_air_temp_sensor"
+
diff --git a/gencode/docs/events_discovery.html b/gencode/docs/events_discovery.html index ed276bf4fc..5c049eab2a 100644 --- a/gencode/docs/events_discovery.html +++ b/gencode/docs/events_discovery.html @@ -2488,6 +2488,68 @@

+
+
+
+

+ +

+
+ +
+
+ + Type: array of string
+

List of target addresses for a partial discovery scan (shard)

+
+ + + + + + No Additional Items

Each item of this array must be:

+
+
+ + + Type: string
+ + + + + + + +
+
+
+
+
+
@@ -12527,16 +12589,18 @@

expr

Type: string

Virtual equipment mapping linking this local point to a remote point in another device

-
Must match regular expression: ^[-0-9a-zA-Z$]+:[a-z0-9_]+$ +Must match regular expression: .*[-_0-9a-zA-Z$]+:[a-z0-9_]+.*
-
Example:
+
Examples:

"VAV-3:supply_air_flowrate_sensor"
 
+
"VAV-101:zone_air_temp_sensor - VAV-102:zone_air_temp_sensor"
+
@@ -42748,16 +42812,18 @@

exprType: string

Virtual equipment mapping linking this local point to a remote point in another device

-
Must match regular expression: ^[-0-9a-zA-Z$]+:[a-z0-9_]+$ +Must match regular expression: .*[-_0-9a-zA-Z$]+:[a-z0-9_]+.*
-
Example:
+
Examples:

"VAV-3:supply_air_flowrate_sensor"
 
+
"VAV-101:zone_air_temp_sensor - VAV-102:zone_air_temp_sensor"
+
@@ -47251,6 +47317,110 @@

+
+
+
+

+ +

+
+ +
+
+ + Type: array of string
+

List of target addresses for a partial discovery scan (shard)

+
+ + + + + + No Additional Items

Each item of this array must be:

+
+
+ + + Type: string
+ + + + + + + +
+
+
+
+
+
@@ -60776,16 +60946,18 @@

expr

Type: string

Virtual equipment mapping linking this local point to a remote point in another device

-
Must match regular expression: ^[-0-9a-zA-Z$]+:[a-z0-9_]+$ +Must match regular expression: .*[-_0-9a-zA-Z$]+:[a-z0-9_]+.*
-
Example:
+
Examples:

"VAV-3:supply_air_flowrate_sensor"
 
+
"VAV-101:zone_air_temp_sensor - VAV-102:zone_air_temp_sensor"
+
diff --git a/gencode/docs/metadata.html b/gencode/docs/metadata.html index 640cf801a7..e17a11d7df 100644 --- a/gencode/docs/metadata.html +++ b/gencode/docs/metadata.html @@ -21120,16 +21120,18 @@

exprType: string

Virtual equipment mapping linking this local point to a remote point in another device

-
Must match regular expression: ^[-0-9a-zA-Z$]+:[a-z0-9_]+$ +Must match regular expression: .*[-_0-9a-zA-Z$]+:[a-z0-9_]+.*
-
Example:
+
Examples:

"VAV-3:supply_air_flowrate_sensor"
 
+
"VAV-101:zone_air_temp_sensor - VAV-102:zone_air_temp_sensor"
+
@@ -25126,6 +25128,96 @@

+
+
+
+

+ +

+
+ +
+
+ + Type: array of string
+

List of target addresses for a partial discovery scan (shard)

+
+ + + + + + No Additional Items

Each item of this array must be:

+
+
+ + + Type: string
+ + + + + + + +
+
+
+
+
+
@@ -37489,16 +37581,18 @@

expr

Type: string

Virtual equipment mapping linking this local point to a remote point in another device

-
Must match regular expression: ^[-0-9a-zA-Z$]+:[a-z0-9_]+$ +Must match regular expression: .*[-_0-9a-zA-Z$]+:[a-z0-9_]+.*
-
Example:
+
Examples:

"VAV-3:supply_air_flowrate_sensor"
 
+
"VAV-101:zone_air_temp_sensor - VAV-102:zone_air_temp_sensor"
+
diff --git a/gencode/java/udmi/schema/DiscoveryEvents.java b/gencode/java/udmi/schema/DiscoveryEvents.java index 095c97b60d..f3aa79c1b4 100644 --- a/gencode/java/udmi/schema/DiscoveryEvents.java +++ b/gencode/java/udmi/schema/DiscoveryEvents.java @@ -2,6 +2,7 @@ package udmi.schema; import java.util.Date; +import java.util.List; import java.util.Map; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; @@ -84,6 +85,13 @@ public class DiscoveryEvents { @JsonProperty("active_count") @JsonPropertyDescription("Total number of records expected for this active scan generation") public Integer active_count; + /** + * List of target addresses for a partial discovery scan (shard) + * + */ + @JsonProperty("addrs") + @JsonPropertyDescription("List of target addresses for a partial discovery scan (shard)") + public List addrs; /** * Links to other address families (family and id) * @@ -158,6 +166,7 @@ public int hashCode() { int result = 1; result = ((result* 31)+((this.generation == null)? 0 :this.generation.hashCode())); result = ((result* 31)+((this.devices == null)? 0 :this.devices.hashCode())); + result = ((result* 31)+((this.addrs == null)? 0 :this.addrs.hashCode())); result = ((result* 31)+((this.source == null)? 0 :this.source.hashCode())); result = ((result* 31)+((this.active_count == null)? 0 :this.active_count.hashCode())); result = ((result* 31)+((this.registries == null)? 0 :this.registries.hashCode())); @@ -187,7 +196,7 @@ public boolean equals(Object other) { return false; } DiscoveryEvents rhs = ((DiscoveryEvents) other); - return ((((((((((((((((((((this.generation == rhs.generation)||((this.generation!= null)&&this.generation.equals(rhs.generation)))&&((this.devices == rhs.devices)||((this.devices!= null)&&this.devices.equals(rhs.devices))))&&((this.source == rhs.source)||((this.source!= null)&&this.source.equals(rhs.source))))&&((this.active_count == rhs.active_count)||((this.active_count!= null)&&this.active_count.equals(rhs.active_count))))&&((this.registries == rhs.registries)||((this.registries!= null)&&this.registries.equals(rhs.registries))))&&((this.families == rhs.families)||((this.families!= null)&&this.families.equals(rhs.families))))&&((this.version == rhs.version)||((this.version!= null)&&this.version.equals(rhs.version))))&&((this.network == rhs.network)||((this.network!= null)&&this.network.equals(rhs.network))))&&((this.points == rhs.points)||((this.points!= null)&&this.points.equals(rhs.points))))&&((this.features == rhs.features)||((this.features!= null)&&this.features.equals(rhs.features))))&&((this.system == rhs.system)||((this.system!= null)&&this.system.equals(rhs.system))))&&((this.event_no == rhs.event_no)||((this.event_no!= null)&&this.event_no.equals(rhs.event_no))))&&((this.refs == rhs.refs)||((this.refs!= null)&&this.refs.equals(rhs.refs))))&&((this.model == rhs.model)||((this.model!= null)&&this.model.equals(rhs.model))))&&((this.family == rhs.family)||((this.family!= null)&&this.family.equals(rhs.family))))&&((this.addr == rhs.addr)||((this.addr!= null)&&this.addr.equals(rhs.addr))))&&((this.cloud_model == rhs.cloud_model)||((this.cloud_model!= null)&&this.cloud_model.equals(rhs.cloud_model))))&&((this.timestamp == rhs.timestamp)||((this.timestamp!= null)&&this.timestamp.equals(rhs.timestamp))))&&((this.status == rhs.status)||((this.status!= null)&&this.status.equals(rhs.status)))); + return (((((((((((((((((((((this.generation == rhs.generation)||((this.generation!= null)&&this.generation.equals(rhs.generation)))&&((this.devices == rhs.devices)||((this.devices!= null)&&this.devices.equals(rhs.devices))))&&((this.addrs == rhs.addrs)||((this.addrs!= null)&&this.addrs.equals(rhs.addrs))))&&((this.source == rhs.source)||((this.source!= null)&&this.source.equals(rhs.source))))&&((this.active_count == rhs.active_count)||((this.active_count!= null)&&this.active_count.equals(rhs.active_count))))&&((this.registries == rhs.registries)||((this.registries!= null)&&this.registries.equals(rhs.registries))))&&((this.families == rhs.families)||((this.families!= null)&&this.families.equals(rhs.families))))&&((this.version == rhs.version)||((this.version!= null)&&this.version.equals(rhs.version))))&&((this.network == rhs.network)||((this.network!= null)&&this.network.equals(rhs.network))))&&((this.points == rhs.points)||((this.points!= null)&&this.points.equals(rhs.points))))&&((this.features == rhs.features)||((this.features!= null)&&this.features.equals(rhs.features))))&&((this.system == rhs.system)||((this.system!= null)&&this.system.equals(rhs.system))))&&((this.event_no == rhs.event_no)||((this.event_no!= null)&&this.event_no.equals(rhs.event_no))))&&((this.refs == rhs.refs)||((this.refs!= null)&&this.refs.equals(rhs.refs))))&&((this.model == rhs.model)||((this.model!= null)&&this.model.equals(rhs.model))))&&((this.family == rhs.family)||((this.family!= null)&&this.family.equals(rhs.family))))&&((this.addr == rhs.addr)||((this.addr!= null)&&this.addr.equals(rhs.addr))))&&((this.cloud_model == rhs.cloud_model)||((this.cloud_model!= null)&&this.cloud_model.equals(rhs.cloud_model))))&&((this.timestamp == rhs.timestamp)||((this.timestamp!= null)&&this.timestamp.equals(rhs.timestamp))))&&((this.status == rhs.status)||((this.status!= null)&&this.status.equals(rhs.status)))); } } diff --git a/gencode/python/udmi/schema/events_discovery.py b/gencode/python/udmi/schema/events_discovery.py index f7674a90c4..cdca6c2c3b 100644 --- a/gencode/python/udmi/schema/events_discovery.py +++ b/gencode/python/udmi/schema/events_discovery.py @@ -4,7 +4,7 @@ from __future__ import annotations from dataclasses import dataclass -from typing import Any, Dict, Optional +from typing import Any, Dict, List, Optional from ._base import DataModel from .ancillary_properties import AncillaryProperties @@ -42,6 +42,7 @@ class DiscoveryEvents(DataModel): network: Optional[str] = None event_no: Optional[int] = None active_count: Optional[int] = None + addrs: Optional[List[str]] = None families: Optional[Dict[str, FamilyDiscovery]] = None registries: Optional[Dict[str, CloudModel]] = None devices: Optional[Dict[str, CloudModel]] = None diff --git a/mcp/butler/provider.py b/mcp/butler/provider.py index 8c5f0c8f12..8c8ad1c2ac 100644 --- a/mcp/butler/provider.py +++ b/mcp/butler/provider.py @@ -342,6 +342,7 @@ def get_discovered_models(self, registry_id: str) -> Dict[str, Dict[str, Any]]: msg_rows = cur.fetchall() conn.close() + gen_seen: Dict[str, set] = {} for dev_id, sub_type, sub_folder, payload in msg_rows: if isinstance(payload, str): try: @@ -354,10 +355,33 @@ def get_discovered_models(self, registry_id: str) -> Dict[str, Dict[str, Any]]: continue if sub_folder == "discovery" and sub_type == "events": - if payload.get("family") == "iot" and isinstance(payload.get("model"), dict): - target_dev = payload.get("addr") or dev_id - if target_dev: - models[str(target_dev)] = payload["model"] + if payload.get("family") == "iot": + gen = str(payload.get("generation") or "") + event_no = payload.get("event_no") + if ( + isinstance(event_no, int) + and event_no == 0 + and not payload.get("addr") + and not isinstance(payload.get("model"), dict) + ): + already_seen = gen_seen.get(gen, set()) if gen else set() + addrs = payload.get("addrs") + if isinstance(addrs, list): + for addr in addrs: + addr_str = str(addr) + if addr_str not in already_seen: + models.pop(addr_str, None) + else: + for existing_id in list(models.keys()): + if existing_id not in already_seen: + models.pop(existing_id, None) + elif isinstance(payload.get("model"), dict): + target_dev = payload.get("addr") or dev_id + if target_dev: + target_str = str(target_dev) + models[target_str] = payload["model"] + if gen: + gen_seen.setdefault(gen, set()).add(target_str) elif sub_type == "model": if dev_id: models[str(dev_id)] = payload diff --git a/schema/events_discovery.json b/schema/events_discovery.json index 169bf8cf8c..5d56afac39 100644 --- a/schema/events_discovery.json +++ b/schema/events_discovery.json @@ -50,6 +50,13 @@ "minimum": 0, "type": "integer" }, + "addrs": { + "description": "List of target addresses for a partial discovery scan (shard)", + "type": "array", + "items": { + "type": "string" + } + }, "families": { "description": "Links to other address families (family and id)", "type": "object", diff --git a/schema/model_pointset_point.json b/schema/model_pointset_point.json index e152f80c6c..c5dcf8aed2 100644 --- a/schema/model_pointset_point.json +++ b/schema/model_pointset_point.json @@ -107,8 +107,8 @@ "expr": { "type": "string", "description": "Virtual equipment mapping linking this local point to a remote point in another device", - "pattern": "^[-0-9a-zA-Z$]+:[a-z0-9_]+$", - "examples": ["VAV-3:supply_air_flowrate_sensor"] + "pattern": ".*[-_0-9a-zA-Z$]+:[a-z0-9_]+.*", + "examples": ["VAV-3:supply_air_flowrate_sensor", "VAV-101:zone_air_temp_sensor - VAV-102:zone_air_temp_sensor"] }, "values": { "type": "object", diff --git a/tests/schemas/events_discovery/model_discovery_start.json b/tests/schemas/events_discovery/model_discovery_start.json index e1ea2b09ad..dfa7d26c6f 100644 --- a/tests/schemas/events_discovery/model_discovery_start.json +++ b/tests/schemas/events_discovery/model_discovery_start.json @@ -4,5 +4,7 @@ "generation": "2026-04-06T20:10:00Z", "event_no": 0, "family": "iot", + "source": "model", + "addrs": ["AHU-1", "AHU-2"], "active_count": 3 } diff --git a/udmis/src/main/java/com/google/bos/udmi/service/core/ModelDiscoveryHandler.java b/udmis/src/main/java/com/google/bos/udmi/service/core/ModelDiscoveryHandler.java index fbf3680eec..2d127d3e90 100644 --- a/udmis/src/main/java/com/google/bos/udmi/service/core/ModelDiscoveryHandler.java +++ b/udmis/src/main/java/com/google/bos/udmi/service/core/ModelDiscoveryHandler.java @@ -1,5 +1,6 @@ package com.google.bos.udmi.service.core; +import static com.google.udmi.util.GeneralUtils.friendlyStackTrace; import static com.google.udmi.util.GeneralUtils.isTrue; import static com.google.udmi.util.JsonUtil.fromString; import static com.google.udmi.util.JsonUtil.isoConvert; @@ -8,11 +9,26 @@ import com.google.bos.udmi.service.access.IotAccessBase; import com.google.bos.udmi.service.pod.UdmiServicePod; import com.google.udmi.util.MetadataMapKeys; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collections; import java.util.Date; +import java.util.Deque; import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Map; import java.util.Map.Entry; +import java.util.Objects; +import java.util.Set; +import java.util.TreeSet; +import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; import udmi.lib.ProtocolFamily; +import udmi.schema.Category; import udmi.schema.CloudModel; import udmi.schema.DiscoveryConfig; import udmi.schema.DiscoveryEvents; @@ -24,7 +40,10 @@ import udmi.schema.FamilyDiscoveryConfig; import udmi.schema.FamilyDiscoveryState; import udmi.schema.FamilyDiscoveryState.Phase; +import udmi.schema.GatewayModel; +import udmi.schema.Level; import udmi.schema.Metadata; +import udmi.schema.PointPointsetModel; /** * Handler for executing Site Model Discovery scans for a device registry. @@ -32,6 +51,10 @@ public class ModelDiscoveryHandler { public static final String MODEL_SOURCE = "model"; + private static final Pattern EXPR_DEVICE_PATTERN = + Pattern.compile("([-_0-9a-zA-Z$]+):[a-z0-9_]+"); + private static final ConcurrentHashMap REGISTRY_LOCKS = + new ConcurrentHashMap<>(); private final ProcessorBase processor; private final IotAccessBase iotAccess; @@ -92,40 +115,111 @@ public static void processDiscovery(ProcessorBase processor, String registryId, new ModelDiscoveryHandler(processor, registryId, familyConfig, generation).process(); } + /** + * Extract target device IDs referenced in pointset point expressions (excluding $SELF). + */ + public static Set extractExpressionReferences(Metadata metadata) { + if (metadata == null || metadata.pointset == null || metadata.pointset.points == null) { + return Set.of(); + } + Set refs = new HashSet<>(); + for (PointPointsetModel point : metadata.pointset.points.values()) { + if (point != null && point.expr != null) { + Matcher matcher = EXPR_DEVICE_PATTERN.matcher(point.expr); + while (matcher.find()) { + String refDevice = matcher.group(1); + if (!refDevice.startsWith("$")) { + refs.add(refDevice); + } + } + } + } + return refs; + } + + /** + * Extract all target device IDs referenced in a device's Metadata model across + * relationships, pointset point expressions, and gateway bindings. + */ + public static Set extractReferencedDevices(Metadata metadata) { + if (metadata == null) { + return Set.of(); + } + Set refs = new HashSet<>(); + if (metadata.relationships != null) { + refs.addAll(metadata.relationships.keySet()); + } + refs.addAll(extractExpressionReferences(metadata)); + if (metadata.gateway != null) { + if (metadata.gateway.gateway_id != null && !metadata.gateway.gateway_id.isEmpty()) { + refs.add(metadata.gateway.gateway_id); + } + if (metadata.gateway.proxy_ids != null) { + metadata.gateway.proxy_ids.stream() + .filter(Objects::nonNull) + .forEach(refs::add); + } + } + return refs; + } + /** * Run the complete model discovery lifecycle: start state bookend, start/count event * (event_no = 0), individual device model events (event_no = 1..N), and stop state bookend. + * Synchronized per registry across all UDMIS processor instances. */ - public synchronized void process() { + public void process() { + synchronized (REGISTRY_LOCKS.computeIfAbsent(registryId, k -> new Object())) { + executeProcess(); + } + } + + private void executeProcess() { processor.info("Starting model discovery for registry %s (generation %s)", registryId, generation); - publishState(Phase.ACTIVE, 0); - - CloudModel registryListing = iotAccess != null ? iotAccess.listDevices(registryId, null) : null; - List activeDevices = (registryListing == null || registryListing.device_ids == null) - ? List.of() - : registryListing.device_ids.entrySet().stream() - .filter(entry -> !isTrue(entry.getValue().blocked)) - .map(Entry::getKey) - .filter(this::matchesTarget) - .sorted() - .toList(); - - int activeCount = activeDevices.size(); - publishStartEvent(activeCount); - - if (shouldDetailEntries()) { - int eventNo = 0; - for (String deviceId : activeDevices) { - publishDeviceEvent(deviceId, ++eventNo); + publishState(Phase.ACTIVE, 0, null); + int activeCount = 0; + + try { + CloudModel registryListing = iotAccess != null + ? iotAccess.listDevices(registryId, null) + : null; + Set siteDevices = (registryListing == null || registryListing.device_ids == null) + ? Set.of() + : registryListing.device_ids.entrySet().stream() + .filter(entry -> !isTrue(entry.getValue().blocked)) + .map(Entry::getKey) + .collect(Collectors.toCollection(TreeSet::new)); + + List shardAddrs = getShardAddrs(); + Map metadataCache = new HashMap<>(); + List closureDevices = resolveClosure(siteDevices, shardAddrs, metadataCache); + + activeCount = closureDevices.size(); + publishStartEvent(activeCount, shardAddrs); + + if (shouldDetailEntries()) { + int eventNo = 0; + for (String deviceId : closureDevices) { + publishDeviceEvent(deviceId, ++eventNo, metadataCache); + } } - } - publishState(Phase.STOPPED, activeCount); + publishState(Phase.STOPPED, activeCount, null); - processor.info("Completed model discovery for registry %s (%d active devices)", - registryId, activeCount); + processor.info("Completed model discovery for registry %s (%d active devices)", + registryId, activeCount); + } catch (Exception e) { + udmi.schema.Entry status = new udmi.schema.Entry(); + status.category = Category.DISCOVERY_FAMILY_SCAN; + status.level = Level.ERROR.value(); + status.message = friendlyStackTrace(e); + status.timestamp = new Date(); + publishState(Phase.STOPPED, activeCount, status); + throw new RuntimeException( + String.format("While executing model discovery for registry %s", registryId), e); + } } private boolean shouldDetailEntries() { @@ -133,9 +227,101 @@ private boolean shouldDetailEntries() { return depth == null || depth == Depth.DETAILS || depth == Depth.PARTS; } - private boolean matchesTarget(String addr) { - List targets = familyConfig != null ? familyConfig.addrs : null; - return targets == null || targets.isEmpty() || (addr != null && targets.contains(addr)); + private List getShardAddrs() { + if (familyConfig == null || familyConfig.addrs == null || familyConfig.addrs.isEmpty()) { + return null; + } + List addrs = familyConfig.addrs.stream() + .filter(Objects::nonNull) + .distinct() + .toList(); + return addrs.isEmpty() ? null : addrs; + } + + private List resolveClosure(Set siteDevices, List shardAddrs, + Map metadataCache) { + if (shardAddrs == null) { + return new ArrayList<>(siteDevices); + } + Set shardSet = new HashSet<>(shardAddrs); + Map> deviceRefs = new HashMap<>(); + Map> exprRefs = new HashMap<>(); + for (String deviceId : siteDevices) { + Metadata metadata = fetchDeviceMetadata(deviceId); + metadataCache.put(deviceId, metadata); + deviceRefs.put(deviceId, new HashSet<>(extractReferencedDevices(metadata))); + exprRefs.put(deviceId, extractExpressionReferences(metadata)); + } + + // Ensure gateway <-> proxy edges are bidirectional even if gateway metadata omitted proxy_ids + for (String deviceId : siteDevices) { + Metadata metadata = metadataCache.get(deviceId); + String gatewayId = (metadata != null && metadata.gateway != null) + ? metadata.gateway.gateway_id + : null; + if (gatewayId != null && !gatewayId.isEmpty() && siteDevices.contains(gatewayId)) { + deviceRefs.computeIfAbsent(gatewayId, k -> new HashSet<>()).add(deviceId); + Metadata gwMetadata = metadataCache.get(gatewayId); + if (gwMetadata != null) { + if (gwMetadata.gateway == null) { + gwMetadata.gateway = new GatewayModel(); + } + Set mergedProxies = new LinkedHashSet<>( + gwMetadata.gateway.proxy_ids != null ? gwMetadata.gateway.proxy_ids : List.of()); + mergedProxies.add(deviceId); + gwMetadata.gateway.proxy_ids = new ArrayList<>(mergedProxies); + } + } + } + + Set closure = new TreeSet<>(); + Deque queue = new ArrayDeque<>(); + + // 1. Seed with active devices in Shard + for (String shardId : shardAddrs) { + if (siteDevices.contains(shardId) && closure.add(shardId)) { + queue.add(shardId); + } + } + + // 2. Seed with 1-hop inbound referrers across all reference types (including to deleted shard + // devices) + for (String deviceId : siteDevices) { + if (!Collections.disjoint(deviceRefs.getOrDefault(deviceId, Set.of()), shardSet)) { + if (closure.add(deviceId)) { + queue.add(deviceId); + } + } + } + + // 3. Seed with transitive inbound referrers across pointset expressions (C -> B -> A) + boolean addedExprInbound = true; + Set exprTargets = new HashSet<>(shardSet); + exprTargets.addAll(closure); + while (addedExprInbound) { + addedExprInbound = false; + for (String deviceId : siteDevices) { + if (!closure.contains(deviceId) + && !Collections.disjoint(exprRefs.getOrDefault(deviceId, Set.of()), exprTargets)) { + closure.add(deviceId); + queue.add(deviceId); + exprTargets.add(deviceId); + addedExprInbound = true; + } + } + } + + // 4. Transitive outbound BFS across all reference types + while (!queue.isEmpty()) { + String current = queue.poll(); + for (String target : deviceRefs.getOrDefault(current, Set.of())) { + if (siteDevices.contains(target) && closure.add(target)) { + queue.add(target); + } + } + } + + return new ArrayList<>(closure); } private Envelope createDiscoveryEnvelope(SubType subType, String deviceId) { @@ -160,12 +346,13 @@ private DiscoveryEvents createDiscoveryEvent(int eventNo) { return event; } - private void publishState(Phase phase, int activeCount) { + private void publishState(Phase phase, int activeCount, udmi.schema.Entry status) { FamilyDiscoveryState familyState = new FamilyDiscoveryState(); familyState.generation = generation; familyState.phase = phase; familyState.active_count = activeCount; familyState.source = MODEL_SOURCE; + familyState.status = status; DiscoveryState discoveryState = new DiscoveryState(); discoveryState.version = UdmiServicePod.UDMI_VERSION; @@ -177,24 +364,47 @@ private void publishState(Phase phase, int activeCount) { processor.publish(createDiscoveryEnvelope(SubType.STATE, null), discoveryState); } - private void publishStartEvent(int activeCount) { + private void publishStartEvent(int activeCount, List shardAddrs) { DiscoveryEvents countEvent = createDiscoveryEvent(0); countEvent.active_count = activeCount; + countEvent.addrs = shardAddrs; processor.publish(createDiscoveryEnvelope(SubType.EVENTS, null), countEvent); } - private void publishDeviceEvent(String deviceId, int eventNo) { + private void publishDeviceEvent(String deviceId, int eventNo, + Map metadataCache) { DiscoveryEvents deviceEvent = createDiscoveryEvent(eventNo); deviceEvent.addr = deviceId; + deviceEvent.model = metadataCache.containsKey(deviceId) + ? metadataCache.get(deviceId) + : fetchDeviceMetadata(deviceId); + processor.publish(createDiscoveryEnvelope(SubType.EVENTS, deviceId), deviceEvent); + } + private Metadata fetchDeviceMetadata(String deviceId) { CloudModel deviceModel = iotAccess != null ? iotAccess.fetchDevice(registryId, deviceId) : null; - String metadataStr = (deviceModel != null && deviceModel.metadata != null) + if (deviceModel == null) { + return null; + } + String metadataStr = deviceModel.metadata != null ? deviceModel.metadata.get(MetadataMapKeys.UDMI_METADATA) : null; - deviceEvent.model = (metadataStr != null && metadataStr.startsWith("{")) + Metadata metadata = (metadataStr != null && !metadataStr.isBlank()) ? fromString(Metadata.class, metadataStr) : null; - - processor.publish(createDiscoveryEnvelope(SubType.EVENTS, deviceId), deviceEvent); + if (deviceModel.gateway != null && deviceModel.gateway.proxy_ids != null + && !deviceModel.gateway.proxy_ids.isEmpty()) { + if (metadata == null) { + metadata = new Metadata(); + } + if (metadata.gateway == null) { + metadata.gateway = new GatewayModel(); + } + Set mergedProxies = new LinkedHashSet<>( + metadata.gateway.proxy_ids != null ? metadata.gateway.proxy_ids : List.of()); + mergedProxies.addAll(deviceModel.gateway.proxy_ids); + metadata.gateway.proxy_ids = new ArrayList<>(mergedProxies); + } + return metadata; } } diff --git a/udmis/src/main/java/com/google/bos/udmi/service/core/ReflectProcessor.java b/udmis/src/main/java/com/google/bos/udmi/service/core/ReflectProcessor.java index 8c0b0f1ac1..92fd38d0de 100644 --- a/udmis/src/main/java/com/google/bos/udmi/service/core/ReflectProcessor.java +++ b/udmis/src/main/java/com/google/bos/udmi/service/core/ReflectProcessor.java @@ -68,6 +68,7 @@ import udmi.schema.Envelope; import udmi.schema.Envelope.SubFolder; import udmi.schema.Envelope.SubType; +import udmi.schema.FamilyDiscoveryConfig; import udmi.schema.Level; import udmi.schema.SystemModel; import udmi.schema.UdmiConfig; @@ -308,7 +309,12 @@ private CloudModel reflectModel(Envelope attributes, CloudModel request) { Date siteTimestamp = (extracted instanceof SiteMetadataUpdate smu) ? smu.timestamp : request.timestamp; Date generation = ofNullable(siteTimestamp).orElseGet(Date::new); - ModelDiscoveryHandler.processDiscovery(this, targetRegistry, null, generation); + FamilyDiscoveryConfig familyConfig = null; + if (request.device_ids != null && !request.device_ids.isEmpty()) { + familyConfig = new FamilyDiscoveryConfig(); + familyConfig.addrs = request.device_ids.keySet().stream().sorted().toList(); + } + ModelDiscoveryHandler.processDiscovery(this, targetRegistry, familyConfig, generation); } return modelResult; } diff --git a/udmis/src/test/java/com/google/bos/udmi/service/core/ReflectProcessorTest.java b/udmis/src/test/java/com/google/bos/udmi/service/core/ReflectProcessorTest.java index 902f04ea36..507de43374 100644 --- a/udmis/src/test/java/com/google/bos/udmi/service/core/ReflectProcessorTest.java +++ b/udmis/src/test/java/com/google/bos/udmi/service/core/ReflectProcessorTest.java @@ -201,6 +201,14 @@ public void modelRegistryUpdateTest() { // Verify 1 SiteMetadataUpdate + 4 Model Discovery messages emitted to udmi_target assertEquals(5, captured.size(), "Expected SiteMetadataUpdate + 4 model discovery messages"); + udmi.schema.DiscoveryEvents startEvent = captured.stream() + .filter(o -> o instanceof udmi.schema.DiscoveryEvents de + && Integer.valueOf(0).equals(de.event_no)) + .map(o -> (udmi.schema.DiscoveryEvents) o) + .findFirst() + .orElseThrow(() -> new AssertionError("Missing event_no=0 DiscoveryEvents")); + org.junit.jupiter.api.Assertions.assertNull(startEvent.addrs); + assertEquals(1, startEvent.active_count); udmi.schema.DiscoveryEvents devEvent = captured.stream() .filter(o -> o instanceof udmi.schema.DiscoveryEvents de && "AHU-1".equals(de.addr)) .map(o -> (udmi.schema.DiscoveryEvents) o) @@ -210,6 +218,49 @@ public void modelRegistryUpdateTest() { assertEquals("SN-AHU-1", devEvent.model.system.serial_no); } + @Test + public void modelRegistryPartialShardUpdateTest() { + CloudModel registryDevices = new CloudModel(); + registryDevices.device_ids = Map.of( + "AHU-1", new CloudModel(), + "VAV-101", new CloudModel(), + "FCU-99", new CloudModel() + ); + when(provider.listDevices(eq(TEST_REGISTRY), any())).thenReturn(registryDevices); + + CloudModel ahuModel = new CloudModel(); + ahuModel.metadata = Map.of("udmi_metadata", + "{\"version\":\"1.5.7\",\"timestamp\":\"2026-07-28T00:00:00Z\"," + + "\"system\":{\"serial_no\":\"SN-AHU-1\"}," + + "\"relationships\":{\"VAV-101\":{\"FEEDS\":[{}]}}}"); + CloudModel vavModel = new CloudModel(); + vavModel.metadata = Map.of("udmi_metadata", + "{\"version\":\"1.5.7\",\"timestamp\":\"2026-07-28T00:00:00Z\"," + + "\"system\":{\"serial_no\":\"SN-VAV-101\"}}"); + CloudModel fcuModel = new CloudModel(); + fcuModel.metadata = Map.of("udmi_metadata", + "{\"version\":\"1.5.7\",\"timestamp\":\"2026-07-28T00:00:00Z\"," + + "\"system\":{\"serial_no\":\"SN-FCU-99\"}}"); + when(provider.fetchDevice(eq(TEST_REGISTRY), eq("AHU-1"))).thenReturn(ahuModel); + when(provider.fetchDevice(eq(TEST_REGISTRY), eq("VAV-101"))).thenReturn(vavModel); + when(provider.fetchDevice(eq(TEST_REGISTRY), eq("FCU-99"))).thenReturn(fcuModel); + + CloudModel requestModel = new CloudModel(); + requestModel.operation = ModelOperation.UPDATE; + requestModel.resource_type = Resource_type.REGISTRY; + requestModel.device_ids = Map.of("AHU-1", new CloudModel()); + activeTestInstance(() -> getReverseDispatcher().publish(makeModelBundle(requestModel))); + + udmi.schema.DiscoveryEvents startEvent = captured.stream() + .filter(o -> o instanceof udmi.schema.DiscoveryEvents de + && Integer.valueOf(0).equals(de.event_no)) + .map(o -> (udmi.schema.DiscoveryEvents) o) + .findFirst() + .orElseThrow(() -> new AssertionError("Missing event_no=0 DiscoveryEvents")); + assertEquals(java.util.List.of("AHU-1"), startEvent.addrs); + assertEquals(2, startEvent.active_count); + } + @Test public void updateAwarenessPayloadLogging() { ByteArrayOutputStream out = new ByteArrayOutputStream(); diff --git a/udmis/src/test/java/com/google/bos/udmi/service/core/UufiProcessorTest.java b/udmis/src/test/java/com/google/bos/udmi/service/core/UufiProcessorTest.java index 722c54352b..0862594297 100644 --- a/udmis/src/test/java/com/google/bos/udmi/service/core/UufiProcessorTest.java +++ b/udmis/src/test/java/com/google/bos/udmi/service/core/UufiProcessorTest.java @@ -414,6 +414,7 @@ public void modelDiscoveryTest() { assertEquals(2, startEvent.active_count); assertEquals("iot", startEvent.family); assertEquals("model", startEvent.source); + org.junit.jupiter.api.Assertions.assertNull(startEvent.addrs); udmi.schema.DiscoveryEvents devEvent1 = events.stream() .filter(e -> Integer.valueOf(1).equals(e.event_no)) @@ -483,6 +484,198 @@ public void modelDiscoveryEntriesAndAddrsTest() { .orElseThrow(() -> new AssertionError("Missing DiscoveryEvents for depth=entries")); assertEquals(0, startEvent.event_no); assertEquals(1, startEvent.active_count); + assertEquals(List.of("AHU-1"), startEvent.addrs); + } + + /** + * Test Partial (Shard) Model Discovery and Closure expansion across relationships, + * multi-hop inbound/outbound pointset expressions ($SELF excluded), gateway proxy_ids merging, + * and inbound referrers (including referrers to deleted shard devices). + */ + @Test + public void modelDiscoveryShardAndClosureTest() { + udmi.schema.CloudModel registryModel = new udmi.schema.CloudModel(); + registryModel.device_ids = Map.of( + "AHU-1", new udmi.schema.CloudModel(), + "VAV-101", new udmi.schema.CloudModel(), + "VAV-102", new udmi.schema.CloudModel(), + "CHW-1", new udmi.schema.CloudModel(), + "CALC-1", new udmi.schema.CloudModel(), + "CALC-2", new udmi.schema.CloudModel(), + "GAT-1", new udmi.schema.CloudModel(), + "FCU-99", new udmi.schema.CloudModel() + ); + org.mockito.Mockito.when(provider.listDevices(org.mockito.ArgumentMatchers.eq("reg-1"), any())) + .thenReturn(registryModel); + + // AHU-1 -> VAV-101 via relationships, plus $SELF:return_air_temp self-reference + udmi.schema.CloudModel ahuModel = new udmi.schema.CloudModel(); + ahuModel.metadata = Map.of("udmi_metadata", + "{\"version\":\"1.5.7\",\"timestamp\":\"2026-07-28T00:00:00Z\"," + + "\"system\":{\"serial_no\":\"SN-AHU-1\"}," + + "\"pointset\":{\"points\":{\"delta_temp\":{\"expr\":\"$SELF:return_air_temp\"}}}," + + "\"relationships\":{\"VAV-101\":{\"FEEDS\":[{}]}}}"); + // VAV-101 -> VAV-102 via pointset.points.*.expr (2-hop transitive outbound), proxied by GAT-1 + udmi.schema.CloudModel vav101Model = new udmi.schema.CloudModel(); + vav101Model.metadata = Map.of("udmi_metadata", + "{\"version\":\"1.5.7\",\"timestamp\":\"2026-07-28T00:00:00Z\"," + + "\"system\":{\"serial_no\":\"SN-VAV-101\"}," + + "\"gateway\":{\"gateway_id\":\"GAT-1\"}," + + "\"pointset\":{\"points\":{\"zone_temp\":{\"expr\":\"VAV-102:supply_air_temp\"}}}}"); + udmi.schema.CloudModel vav102Model = new udmi.schema.CloudModel(); + vav102Model.metadata = Map.of("udmi_metadata", + "{\"version\":\"1.5.7\",\"timestamp\":\"2026-07-28T00:00:00Z\"," + + "\"system\":{\"serial_no\":\"SN-VAV-102\"}}"); + // CHW-1 references DELETED-DEV (inbound reference to a deleted device in Shard) + udmi.schema.CloudModel chwModel = new udmi.schema.CloudModel(); + chwModel.metadata = Map.of("udmi_metadata", + "{\"version\":\"1.5.7\",\"timestamp\":\"2026-07-28T00:00:00Z\"," + + "\"system\":{\"serial_no\":\"SN-CHW-1\"}," + + "\"relationships\":{\"DELETED-DEV\":{\"FEEDS\":[{}]}}}"); + // 2-hop transitive inbound expr chain: CALC-2 -> CALC-1 -> AHU-1 + udmi.schema.CloudModel calc1Model = new udmi.schema.CloudModel(); + calc1Model.metadata = Map.of("udmi_metadata", + "{\"version\":\"1.5.7\",\"timestamp\":\"2026-07-28T00:00:00Z\"," + + "\"system\":{\"serial_no\":\"SN-CALC-1\"}," + + "\"pointset\":{\"points\":{\"avg_temp\":{\"expr\":\"AHU-1:supply_air_temp\"}}}}"); + udmi.schema.CloudModel calc2Model = new udmi.schema.CloudModel(); + calc2Model.metadata = Map.of("udmi_metadata", + "{\"version\":\"1.5.7\",\"timestamp\":\"2026-07-28T00:00:00Z\"," + + "\"system\":{\"serial_no\":\"SN-CALC-2\"}," + + "\"pointset\":{\"points\":{\"rollup_temp\":{\"expr\":\"CALC-1:avg_temp\"}}}}"); + // GAT-1 omits proxy_ids in static metadata, but CloudModel.gateway.proxy_ids has VAV-101 + udmi.schema.CloudModel gatModel = new udmi.schema.CloudModel(); + gatModel.gateway = new udmi.schema.GatewayModel(); + gatModel.gateway.proxy_ids = List.of("VAV-101"); + gatModel.metadata = Map.of("udmi_metadata", + "{\"version\":\"1.5.7\",\"timestamp\":\"2026-07-28T00:00:00Z\"," + + "\"system\":{\"serial_no\":\"SN-GAT-1\"}}"); + // FCU-99 is completely unrelated to the Shard + udmi.schema.CloudModel fcuModel = new udmi.schema.CloudModel(); + fcuModel.metadata = Map.of("udmi_metadata", + "{\"version\":\"1.5.7\",\"timestamp\":\"2026-07-28T00:00:00Z\"," + + "\"system\":{\"serial_no\":\"SN-FCU-99\"}}"); + + org.mockito.Mockito.when(provider.fetchDevice("reg-1", "AHU-1")).thenReturn(ahuModel); + org.mockito.Mockito.when(provider.fetchDevice("reg-1", "VAV-101")).thenReturn(vav101Model); + org.mockito.Mockito.when(provider.fetchDevice("reg-1", "VAV-102")).thenReturn(vav102Model); + org.mockito.Mockito.when(provider.fetchDevice("reg-1", "CHW-1")).thenReturn(chwModel); + org.mockito.Mockito.when(provider.fetchDevice("reg-1", "CALC-1")).thenReturn(calc1Model); + org.mockito.Mockito.when(provider.fetchDevice("reg-1", "CALC-2")).thenReturn(calc2Model); + org.mockito.Mockito.when(provider.fetchDevice("reg-1", "GAT-1")).thenReturn(gatModel); + org.mockito.Mockito.when(provider.fetchDevice("reg-1", "FCU-99")).thenReturn(fcuModel); + + Envelope innerEnvelope = new Envelope(); + innerEnvelope.subType = SubType.CONFIG; + innerEnvelope.subFolder = SubFolder.DISCOVERY; + innerEnvelope.deviceRegistryId = "reg-1"; + + Map uufiWrapper = toMap(innerEnvelope); + uufiWrapper.put("payload", Map.of( + "version", "1.5.7", + "timestamp", "2026-07-28T00:00:00Z", + "families", Map.of("iot", Map.of( + "generation", "2026-07-28T00:00:00Z", + "depth", "details", + "addrs", List.of("AHU-1", "DELETED-DEV"), + "source", "model" + )) + )); + + Envelope transportEnvelope = new Envelope(); + transportEnvelope.source = "test-client"; + transportEnvelope.gatewayId = "uufi"; + transportEnvelope.deviceRegistryId = "reg-1"; + transportEnvelope.subType = SubType.CONFIG; + transportEnvelope.subFolder = SubFolder.DISCOVERY; + + activeTestInstance(() -> getReverseDispatcher().publish( + new Bundle(transportEnvelope, uufiWrapper))); + + List events = captured.stream() + .filter(o -> o instanceof udmi.schema.DiscoveryEvents) + .map(o -> (udmi.schema.DiscoveryEvents) o) + .toList(); + // event_no=0 + 7 Closure devices (AHU-1, CALC-1, CALC-2, CHW-1, GAT-1, VAV-101, VAV-102); + // FCU-99 is excluded. + assertEquals(8, events.size(), "Expected event_no=0 plus 7 Closure device events"); + + udmi.schema.DiscoveryEvents startEvent = events.stream() + .filter(e -> Integer.valueOf(0).equals(e.event_no)) + .findFirst() + .orElseThrow(); + assertEquals(List.of("AHU-1", "DELETED-DEV"), startEvent.addrs); + assertEquals(7, startEvent.active_count); + + List discoveredAddrs = events.stream() + .filter(e -> e.event_no > 0) + .sorted(java.util.Comparator.comparingInt(e -> e.event_no)) + .map(e -> e.addr) + .toList(); + assertEquals( + List.of("AHU-1", "CALC-1", "CALC-2", "CHW-1", "GAT-1", "VAV-101", "VAV-102"), + discoveredAddrs); + + udmi.schema.DiscoveryEvents gatEvent = events.stream() + .filter(e -> "GAT-1".equals(e.addr)) + .findFirst() + .orElseThrow(); + assertNotNull(gatEvent.model.gateway); + assertEquals(List.of("VAV-101"), gatEvent.model.gateway.proxy_ids); + } + + /** + * Test that corrupt udmi_metadata fails fast and publishes a terminal STOPPED state/discovery + * bookend with an error status entry. + */ + @Test + public void modelDiscoveryFailurePublishesStoppedErrorStateTest() { + udmi.schema.CloudModel registryModel = new udmi.schema.CloudModel(); + registryModel.device_ids = Map.of("AHU-BAD", new udmi.schema.CloudModel()); + org.mockito.Mockito.when(provider.listDevices(org.mockito.ArgumentMatchers.eq("reg-1"), any())) + .thenReturn(registryModel); + + udmi.schema.CloudModel badModel = new udmi.schema.CloudModel(); + badModel.metadata = Map.of("udmi_metadata", "{not-valid-json"); + org.mockito.Mockito.when(provider.fetchDevice("reg-1", "AHU-BAD")).thenReturn(badModel); + + Envelope innerEnvelope = new Envelope(); + innerEnvelope.subType = SubType.CONFIG; + innerEnvelope.subFolder = SubFolder.DISCOVERY; + innerEnvelope.deviceRegistryId = "reg-1"; + + Map uufiWrapper = toMap(innerEnvelope); + uufiWrapper.put("payload", Map.of( + "version", "1.5.7", + "timestamp", "2026-07-28T00:00:00Z", + "families", Map.of("iot", Map.of( + "generation", "2026-07-28T00:00:00Z", + "depth", "details", + "source", "model" + )) + )); + + Envelope transportEnvelope = new Envelope(); + transportEnvelope.source = "test-client"; + transportEnvelope.gatewayId = "uufi"; + transportEnvelope.deviceRegistryId = "reg-1"; + transportEnvelope.subType = SubType.CONFIG; + transportEnvelope.subFolder = SubFolder.DISCOVERY; + + activeTestInstance(() -> getReverseDispatcher().publish( + new Bundle(transportEnvelope, uufiWrapper))); + + List states = captured.stream() + .filter(o -> o instanceof udmi.schema.DiscoveryState) + .map(o -> (udmi.schema.DiscoveryState) o) + .toList(); + assertEquals(2, states.size(), "Expected ACTIVE and terminal STOPPED DiscoveryState on error"); + udmi.schema.DiscoveryState stopState = states.get(1); + assertEquals(udmi.schema.FamilyDiscoveryState.Phase.STOPPED, + stopState.families.get("iot").phase); + assertNotNull(stopState.families.get("iot").status); + assertEquals(udmi.schema.Category.DISCOVERY_FAMILY_SCAN, + stopState.families.get("iot").status.category); } private UufiProcessor getProcessor() { diff --git a/validator/src/main/java/com/google/daq/mqtt/registrar/Registrar.java b/validator/src/main/java/com/google/daq/mqtt/registrar/Registrar.java index 315d4966c0..e71f6e52db 100644 --- a/validator/src/main/java/com/google/daq/mqtt/registrar/Registrar.java +++ b/validator/src/main/java/com/google/daq/mqtt/registrar/Registrar.java @@ -461,11 +461,14 @@ protected Map getWorkingDevices() { return workingDevices; } + private Set targetShardDevices; + private void processSiteMetadata() { SiteMetadata siteMetadata = siteModel.loadSiteMetadata(); siteMetadata.name = ofNullable(siteMetadata.name).orElse(siteModel.getSiteName()); ifTrueThen(updateCloudIoT, - () -> cloudIotManager.updateRegistry(getSiteMetadata(), ModelOperation.UPDATE)); + () -> cloudIotManager.updateRegistry(getSiteMetadata(), ModelOperation.UPDATE, + targetShardDevices)); } private SiteMetadata getSiteMetadata() { @@ -508,7 +511,9 @@ private void setValidateMetadata() { private void setDeviceList(List deviceList) { this.deviceList = deviceList; - blockUnknown = false; + if (blockUnknown == null) { + blockUnknown = false; + } } @CommandLineOption(short_form = "-f", arg_name = "topic", description = "Set PubSub feed topic") @@ -665,17 +670,27 @@ private void processAllDevices(Runnable modelMunger) { ifNotNullThen(modelMunger, Runnable::run); initializeLocalDevices(); updateExplicitDevices(explicitDevices, workingDevices); + targetShardDevices = explicitDevices != null ? new HashSet<>(explicitDevices) : null; cloudModels = ifNotNullGet(fetchCloudModels(), devices -> new ConcurrentHashMap<>(devices)); if (deleteDevices || expungeDevices) { deleteCloudDevices(); return; } + Set targetedExtras = ImmutableSet.of(); if (explicitDevices != null) { - Set unknownLocals = difference(explicitDevices, workingDevices.keySet()); + Set unknownLocals = + new HashSet<>(difference(explicitDevices, workingDevices.keySet())); if (!unknownLocals.isEmpty()) { - throw new RuntimeException( - "Unknown specified devices: " + JOIN_CSV.join(unknownLocals)); + if (isTrue(blockUnknown) && cloudModels != null) { + targetedExtras = new HashSet<>(intersection(unknownLocals, cloudModels.keySet())); + unknownLocals.removeAll(targetedExtras); + explicitDevices.removeAll(targetedExtras); + } + if (!unknownLocals.isEmpty()) { + throw new RuntimeException( + "Unknown specified devices: " + JOIN_CSV.join(unknownLocals)); + } } } @@ -709,8 +724,12 @@ private void processAllDevices(Runnable modelMunger) { finalizeLocalDevices(); - if (cloudModels != null && !isTargeted && !instantiateExtras) { - extraDevices = processExtraDevices(difference(cloudModels.keySet(), targetDevices)); + if (cloudModels != null && !instantiateExtras) { + if (!isTargeted) { + extraDevices = processExtraDevices(difference(cloudModels.keySet(), targetDevices)); + } else if (!targetedExtras.isEmpty()) { + extraDevices = processExtraDevices(targetedExtras); + } } } catch (Exception e) { throw new RuntimeException("While processing devices", e); @@ -1130,7 +1149,9 @@ private Set processExtraDevices(Set extraDevices) { dynamicTerminate(extraDevices.size()); System.err.printf("There were %d/%d already blocked devices.%n", alreadyBlocked.get(), extraDevices.size()); - reapExtraDevices(difference(siteModel.getExtraDevices(), extraDevices)); + if (targetShardDevices == null) { + reapExtraDevices(difference(siteModel.getExtraDevices(), extraDevices)); + } return extras.keySet(); } catch (Exception e) { throw new RuntimeException(format("Processing %d extra devices", extraDevices.size()), e); diff --git a/validator/src/main/java/com/google/daq/mqtt/util/CloudIotManager.java b/validator/src/main/java/com/google/daq/mqtt/util/CloudIotManager.java index d83fbd8eba..37eb4dd714 100644 --- a/validator/src/main/java/com/google/daq/mqtt/util/CloudIotManager.java +++ b/validator/src/main/java/com/google/daq/mqtt/util/CloudIotManager.java @@ -32,6 +32,7 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; import udmi.schema.CloudModel; import udmi.schema.CloudModel.ModelOperation; import udmi.schema.CloudModel.Resource_type; @@ -450,6 +451,14 @@ public String createRegistry(String suffix) { * Update the cloud site metadata for the current registry. */ public void updateRegistry(SiteMetadata siteMetadata, ModelOperation operation) { + updateRegistry(siteMetadata, operation, null); + } + + /** + * Update the cloud site metadata for the current registry with optional explicit device shard. + */ + public void updateRegistry(SiteMetadata siteMetadata, ModelOperation operation, + Set explicitDevices) { CloudModel registryModel = new CloudModel(); registryModel.resource_type = Resource_type.REGISTRY; registryModel.operation = operation; @@ -461,6 +470,10 @@ MetadataMapKeys.UDMI_METADATA, toJsonString(siteMetadata) MetadataMapKeys.UDMI_PROVISIONED, isoConvert(ofNullable(siteMetadata.timestamp).orElseGet(Date::new)) ); + if (explicitDevices != null && !explicitDevices.isEmpty()) { + registryModel.device_ids = explicitDevices.stream() + .collect(Collectors.toMap(id -> id, id -> new CloudModel())); + } getIotProvider().updateRegistry(registryModel); } diff --git a/validator/src/test/java/com/google/daq/mqtt/registrar/RegistrarTest.java b/validator/src/test/java/com/google/daq/mqtt/registrar/RegistrarTest.java index 4930f9716a..9cd2042dbf 100644 --- a/validator/src/test/java/com/google/daq/mqtt/registrar/RegistrarTest.java +++ b/validator/src/test/java/com/google/daq/mqtt/registrar/RegistrarTest.java @@ -11,6 +11,7 @@ import static com.google.daq.mqtt.util.IotMockProvider.ActionType.CREATE_DEVICE_ACTION; import static com.google.daq.mqtt.util.IotMockProvider.ActionType.DELETE_DEVICE_ACTION; import static com.google.daq.mqtt.util.IotMockProvider.ActionType.UPDATE_DEVICE_ACTION; +import static com.google.daq.mqtt.util.IotMockProvider.ActionType.UPDATE_REGISTRY_ACTION; import static com.google.daq.mqtt.util.IotMockProvider.MOCK_DEVICE_ID; import static com.google.udmi.util.GeneralUtils.friendlyStackTrace; import static com.google.udmi.util.GeneralUtils.ifNotNullThen; @@ -115,6 +116,16 @@ public void blockDevicesTest() { Collectors.toSet()).size()); blockActions.forEach(action -> assertEquals("device blocked " + action.deviceId, action.deviceId.equals(MOCK_DEVICE_ID), action.data)); + + List targetedActions = executeRegistrarPopulated( + ImmutableList.of("-b", DEVICE_ID, MOCK_DEVICE_ID)); + List targetedBlocks = filterActions(targetedActions, BLOCK_DEVICE_ACTION); + assertEquals("targeted block action count", 1, targetedBlocks.size()); + assertEquals(MOCK_DEVICE_ID, targetedBlocks.get(0).deviceId); + List registryUpdates = filterActions(targetedActions, UPDATE_REGISTRY_ACTION); + assertEquals(1, registryUpdates.size()); + CloudModel regModel = (CloudModel) registryUpdates.get(0).data; + assertEquals(ImmutableSet.of(DEVICE_ID, MOCK_DEVICE_ID), regModel.device_ids.keySet()); } @Test @@ -197,6 +208,12 @@ public void transitiveUpdate() { int transitiveCreates = transitiveCreate.stream() .filter(mock -> !mock.deviceId.equals(GATEWAY_ID)).toList().size(); assertEquals("Transitive devices", 3, transitiveCreates); + + List registryUpdates = filterActions(transitiveMocked, UPDATE_REGISTRY_ACTION); + assertEquals(1, registryUpdates.size()); + CloudModel regModel = (CloudModel) registryUpdates.get(0).data; + assertEquals("Registry update shard should include transitive devices", + 4, regModel.device_ids.size()); } @Test From 09e549ad1124a0b41b7879ca29c70f2b98a73ad2 Mon Sep 17 00:00:00 2001 From: Khyati Mahendru Date: Thu, 22 Oct 2026 14:38:44 +0000 Subject: [PATCH 6/7] fix(discovery): address PR #1453 review findings for entries replay, state filtering, and expunge shard --- butler/tests/test_handlers.py | 11 ++++ mcp/butler/provider.py | 60 +++++++++++++++---- .../service/core/ModelDiscoveryHandler.java | 17 ++++++ .../udmi/service/core/TargetProcessor.java | 5 +- .../service/core/TargetProcessorTest.java | 42 +++++++++++++ .../google/daq/mqtt/registrar/Registrar.java | 4 ++ .../google/daq/mqtt/util/IotMockProvider.java | 9 ++- .../daq/mqtt/registrar/RegistrarTest.java | 11 ++++ 8 files changed, 140 insertions(+), 19 deletions(-) diff --git a/butler/tests/test_handlers.py b/butler/tests/test_handlers.py index d2ac5794f4..bb0d9c43af 100644 --- a/butler/tests/test_handlers.py +++ b/butler/tests/test_handlers.py @@ -279,6 +279,17 @@ def test_provider_discovered_models_deletion_reconciliation(self): "model": {"system": {"serial_no": "SN-NEW"}}, }, ), + ( + None, + "events", + "discovery", + { + "family": "iot", + "generation": "2026-08-21T02:00:00Z", + "event_no": 0, + "active_count": 2, + }, + ), ], ] self.mock_pg.get_connection.return_value = mock_conn diff --git a/mcp/butler/provider.py b/mcp/butler/provider.py index 8c8ad1c2ac..a5b736404f 100644 --- a/mcp/butler/provider.py +++ b/mcp/butler/provider.py @@ -342,7 +342,28 @@ def get_discovered_models(self, registry_id: str) -> Dict[str, Dict[str, Any]]: msg_rows = cur.fetchall() conn.close() - gen_seen: Dict[str, set] = {} + gen_batches: Dict[str, Dict[str, Any]] = {} + + def _reconcile_gen(gen_key: str) -> None: + batch = gen_batches.get(gen_key) + if not batch or not batch.get("has_start"): + return + active_count = batch.get("active_count") + seen = batch.get("seen", set()) + if not isinstance(active_count, int) or len(seen) < active_count: + return + gen_batches.pop(gen_key, None) + addrs = batch.get("addrs") + if isinstance(addrs, list): + for addr in addrs: + addr_str = str(addr) + if addr_str not in seen: + models.pop(addr_str, None) + else: + for existing_id in list(models.keys()): + if existing_id not in seen: + models.pop(existing_id, None) + for dev_id, sub_type, sub_folder, payload in msg_rows: if isinstance(payload, str): try: @@ -364,24 +385,37 @@ def get_discovered_models(self, registry_id: str) -> Dict[str, Dict[str, Any]]: and not payload.get("addr") and not isinstance(payload.get("model"), dict) ): - already_seen = gen_seen.get(gen, set()) if gen else set() - addrs = payload.get("addrs") - if isinstance(addrs, list): - for addr in addrs: - addr_str = str(addr) - if addr_str not in already_seen: - models.pop(addr_str, None) - else: - for existing_id in list(models.keys()): - if existing_id not in already_seen: - models.pop(existing_id, None) + if gen: + batch = gen_batches.setdefault( + gen, + { + "has_start": False, + "active_count": 0, + "addrs": None, + "seen": set(), + }, + ) + batch["has_start"] = True + batch["active_count"] = payload.get("active_count", 0) + batch["addrs"] = payload.get("addrs") + _reconcile_gen(gen) elif isinstance(payload.get("model"), dict): target_dev = payload.get("addr") or dev_id if target_dev: target_str = str(target_dev) models[target_str] = payload["model"] if gen: - gen_seen.setdefault(gen, set()).add(target_str) + batch = gen_batches.setdefault( + gen, + { + "has_start": False, + "active_count": 0, + "addrs": None, + "seen": set(), + }, + ) + batch["seen"].add(target_str) + _reconcile_gen(gen) elif sub_type == "model": if dev_id: models[str(dev_id)] = payload diff --git a/udmis/src/main/java/com/google/bos/udmi/service/core/ModelDiscoveryHandler.java b/udmis/src/main/java/com/google/bos/udmi/service/core/ModelDiscoveryHandler.java index 2d127d3e90..17abe93723 100644 --- a/udmis/src/main/java/com/google/bos/udmi/service/core/ModelDiscoveryHandler.java +++ b/udmis/src/main/java/com/google/bos/udmi/service/core/ModelDiscoveryHandler.java @@ -88,6 +88,23 @@ public static boolean isModelDiscoveryConfig(Envelope envelope, DiscoveryConfig return iotConfig != null && MODEL_SOURCE.equals(iotConfig.source); } + /** + * Determine whether a target message (event or state) originated from Site Model Discovery. + */ + public static boolean isModelDiscoveryMessage(Envelope envelope, Object message) { + if (envelope != null && MODEL_SOURCE.equals(envelope.source)) { + return true; + } + if (message instanceof DiscoveryEvents de) { + return MODEL_SOURCE.equals(de.source); + } + if (message instanceof DiscoveryState ds && ds.families != null) { + return ds.families.values().stream() + .anyMatch(family -> family != null && MODEL_SOURCE.equals(family.source)); + } + return false; + } + /** * Process a DiscoveryConfig targeting model discovery. */ diff --git a/udmis/src/main/java/com/google/bos/udmi/service/core/TargetProcessor.java b/udmis/src/main/java/com/google/bos/udmi/service/core/TargetProcessor.java index 40dc173354..30a29c4de9 100644 --- a/udmis/src/main/java/com/google/bos/udmi/service/core/TargetProcessor.java +++ b/udmis/src/main/java/com/google/bos/udmi/service/core/TargetProcessor.java @@ -14,7 +14,6 @@ import java.util.Date; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; -import udmi.schema.DiscoveryEvents; import udmi.schema.EndpointConfiguration; import udmi.schema.Envelope; import udmi.schema.Envelope.SubFolder; @@ -47,9 +46,7 @@ protected void defaultHandler(Object defaultedMessage) { publish(defaultedMessage); - if (ModelDiscoveryHandler.MODEL_SOURCE.equals(envelope.source) - || (defaultedMessage instanceof DiscoveryEvents de - && ModelDiscoveryHandler.MODEL_SOURCE.equals(de.source))) { + if (ModelDiscoveryHandler.isModelDiscoveryMessage(envelope, defaultedMessage)) { debug("Skipping reflector forwarding for model discovery message"); return; } diff --git a/udmis/src/test/java/com/google/bos/udmi/service/core/TargetProcessorTest.java b/udmis/src/test/java/com/google/bos/udmi/service/core/TargetProcessorTest.java index 5942333d64..c5176bf050 100644 --- a/udmis/src/test/java/com/google/bos/udmi/service/core/TargetProcessorTest.java +++ b/udmis/src/test/java/com/google/bos/udmi/service/core/TargetProcessorTest.java @@ -94,4 +94,46 @@ public void simpleReceive() { verifyCommand(commandCaptor, false); } + private final java.util.List notices = new java.util.concurrent.CopyOnWriteArrayList<>(); + + @Override + protected ProcessorBase getProcessor(udmi.schema.EndpointConfiguration config, + Class clazz) { + if (clazz.equals(TargetProcessor.class)) { + return new TargetProcessor(config) { + @Override + public void notice(String message) { + notices.add(message); + super.notice(message); + } + }; + } + return super.getProcessor(config, clazz); + } + + @Test + public void modelDiscoveryStateSkippedTest() { + notices.clear(); + initializeTestInstance(); + + udmi.schema.FamilyDiscoveryState familyState = new udmi.schema.FamilyDiscoveryState(); + familyState.source = ModelDiscoveryHandler.MODEL_SOURCE; + udmi.schema.DiscoveryState state = new udmi.schema.DiscoveryState(); + state.families = new java.util.HashMap<>(); + state.families.put("iot", familyState); + + Bundle bundle = new Bundle(state); + bundle.envelope.deviceRegistryId = TEST_REGISTRY; + bundle.envelope.deviceId = null; + bundle.envelope.subType = SubType.STATE; + bundle.envelope.subFolder = SubFolder.DISCOVERY; + bundle.envelope.source = "implicit"; + + getReverseDispatcher().publish(bundle); + terminateAndWait(); + + assertTrue(notices.isEmpty(), + "Registry-level model discovery state should not emit a notice: " + notices); + } + } \ No newline at end of file diff --git a/validator/src/main/java/com/google/daq/mqtt/registrar/Registrar.java b/validator/src/main/java/com/google/daq/mqtt/registrar/Registrar.java index e71f6e52db..1ba4a34319 100644 --- a/validator/src/main/java/com/google/daq/mqtt/registrar/Registrar.java +++ b/validator/src/main/java/com/google/daq/mqtt/registrar/Registrar.java @@ -845,6 +845,10 @@ private void deleteCloudDevices() { throw new IllegalStateException("Neither delete nor expunge indicated"); } + if (targetShardDevices == null && !(deleteDevices && expungeDevices) && !toDelete.isEmpty()) { + targetShardDevices = new HashSet<>(toDelete); + } + executeDelete(toDelete, reason); if (expungeDevices) { diff --git a/validator/src/main/java/com/google/daq/mqtt/util/IotMockProvider.java b/validator/src/main/java/com/google/daq/mqtt/util/IotMockProvider.java index b938efbf10..1ff14777df 100644 --- a/validator/src/main/java/com/google/daq/mqtt/util/IotMockProvider.java +++ b/validator/src/main/java/com/google/daq/mqtt/util/IotMockProvider.java @@ -48,7 +48,10 @@ public IotMockProvider(ExecutionConfiguration executionConfiguration) { siteModel = new SiteModel(executionConfiguration); siteModel.initialize(); ifNotTrueThen(executionConfiguration.project_id.equals(SiteModel.MOCK_CLEAN), - () -> siteModel.forEachDeviceId(this::populateCloudModel)); + () -> { + siteModel.forEachDeviceId(this::populateCloudModel); + populateCloudModel(MOCK_DEVICE_ID); + }); client = mockClientString(executionConfiguration.project_id, siteModel.getRegistryId(), siteModel.getCloudRegion()); } @@ -141,7 +144,9 @@ public Map fetchCloudModels(String forGatewayId) { Map siteDeviceModels = siteModel.allMetadata().entrySet().stream().collect( Collectors.toMap(Entry::getKey, model -> makeCloudModel(model.getValue()))); Map deviceModels = new HashMap<>(siteDeviceModels); - deviceModels.put(MOCK_DEVICE_ID, makeMockModel(MOCK_DEVICE_ID)); + if (cloudDevices.containsKey(MOCK_DEVICE_ID)) { + deviceModels.put(MOCK_DEVICE_ID, makeMockModel(MOCK_DEVICE_ID)); + } if (forGatewayId != null) { deviceModels.remove(forGatewayId); deviceModels.remove(PROXY_DEVICE_ID); diff --git a/validator/src/test/java/com/google/daq/mqtt/registrar/RegistrarTest.java b/validator/src/test/java/com/google/daq/mqtt/registrar/RegistrarTest.java index 9cd2042dbf..3b4f8275c3 100644 --- a/validator/src/test/java/com/google/daq/mqtt/registrar/RegistrarTest.java +++ b/validator/src/test/java/com/google/daq/mqtt/registrar/RegistrarTest.java @@ -126,6 +126,17 @@ public void blockDevicesTest() { assertEquals(1, registryUpdates.size()); CloudModel regModel = (CloudModel) registryUpdates.get(0).data; assertEquals(ImmutableSet.of(DEVICE_ID, MOCK_DEVICE_ID), regModel.device_ids.keySet()); + + List expungeActions = executeRegistrarPopulated(ImmutableList.of("-x")); + List expungeDeletes = filterActions(expungeActions, DELETE_DEVICE_ACTION); + assertEquals("expunge delete action count", 1, expungeDeletes.size()); + assertEquals(MOCK_DEVICE_ID, expungeDeletes.get(0).deviceId); + List expungeRegUpdates = filterActions(expungeActions, UPDATE_REGISTRY_ACTION); + assertEquals(1, expungeRegUpdates.size()); + CloudModel expungeRegModel = (CloudModel) expungeRegUpdates.get(0).data; + assertNotNull("Unscoped -x should populate device_ids shard with expunged devices", + expungeRegModel.device_ids); + assertEquals(ImmutableSet.of(MOCK_DEVICE_ID), expungeRegModel.device_ids.keySet()); } @Test From f974ca77c95883c1ea981eb1158aae951ba78da2 Mon Sep 17 00:00:00 2001 From: Khyati Mahendru Date: Thu, 22 Oct 2026 15:04:57 +0000 Subject: [PATCH 7/7] Clarify that active_count and addrs in events_discovery are only populated on event_no=0 --- .gencode_hash.txt | 8 ++++---- gencode/docs/events.html | 4 ++-- gencode/docs/events_discovery.html | 8 ++++---- gencode/docs/metadata.html | 4 ++-- gencode/java/udmi/schema/DiscoveryEvents.java | 8 ++++---- schema/events_discovery.json | 4 ++-- 6 files changed, 18 insertions(+), 18 deletions(-) diff --git a/.gencode_hash.txt b/.gencode_hash.txt index 0871df9a2b..d1b9dbf071 100644 --- a/.gencode_hash.txt +++ b/.gencode_hash.txt @@ -7,15 +7,15 @@ c21266e8de23b6e8acfac16a177db782375bf71df470c343c3f8eb8b822ef56a gencode/docs/c 94bb566ac6a4b6b7f89cd2cf1911f658446cc0bea405014275104623f9964dd3 gencode/docs/configuration_pubber.html 1057fa40fb7a31a23bb2773d21c38cf4590a935bd8b5ea4218e695c6204f5dd9 gencode/docs/data_template.html 1d5294b4a1fea6555e23144e594523e4793c795069b6880ce607ad09a6015581 gencode/docs/equipment_translation.html -cbcf08f38e9601db5e476ca0f47a54eb25f30f78717cd328b1fcc15b67c26891 gencode/docs/events.html +761b5aeae3b11377feb9b39d3be9a4383c5ed894a119268f44507d894f1b15f0 gencode/docs/events.html 70e57ad6ef39330d958727ebf9dcd61ef6ea30e4c8653eac412bf1867fdb3a70 gencode/docs/events_alarmset.html -a69403cdca757a198785bb52766c1601e76ed5e52ea0ea99c3ca572740ea2531 gencode/docs/events_discovery.html +aa6e65cb3fbaa45e7e51013d579b0e643933886f1ecd61672ee3ffed8830e552 gencode/docs/events_discovery.html eafcc3c48189f605f114cde051fea9d13fc5f1a3e395d64fb0a91cb53d4c9aeb gencode/docs/events_pointset.html b65ce6e8061e61ba676a72c624f1c425bd238e152ce8e3ebb1e9c23f0e502bcb gencode/docs/events_streams.html 81043ba732caab75704d38e6977b32b1c2bcdad89d94ad2a259d0e70ca4aa40e gencode/docs/events_system.html 46fd37b577c655965346d1e3ececf559c4384791214900eeeea1ad8fc72a02b8 gencode/docs/events_udmi.html baf59590b1a71a49e59d8f499ae5112a1b7a555294dbd72bc821203aecceefa3 gencode/docs/events_validation.html -c1f67617826bb9ce98ced54a512d7d45939733ff87cc9b5449484322dde13e1e gencode/docs/metadata.html +b5ae61a5246f37bb2c94fff7ed7c90c05fb9e3b926c420307d4c732c5a31521f gencode/docs/metadata.html 18178a591940c176371fb9196048eaacd40f1323997cf907ca11eb879a095c7e gencode/docs/monitoring.html 64d21cf916e20566171cf9d723e59c297b576bcba89e2a73aec2b018398cc7da gencode/docs/mosquitto_client_response.html 6f332da9cc64230df3551f9748a4eaa4060146e767cae967fc0e73b1f348be5b gencode/docs/persistent_device.html @@ -66,7 +66,7 @@ c04302f973508295258d4d3a29b5da70bf3cd3a6e2c63a958fb70dd3d370276c gencode/java/u 52ae2afb2dcb7863259e7eea71888811663c010429c09760493f5f006a660c84 gencode/java/udmi/schema/Dimension.java 2bb1513817eea9c0486b28f3316ead16d53885a8bad9f6a42ee3d5f762335a68 gencode/java/udmi/schema/DiscoveryCommand.java a572f17a61584a32ff19968356f7f7d7bcc8430131ac3701878d0bd221e1a1fa gencode/java/udmi/schema/DiscoveryConfig.java -5d93ed972e510262af1384c1b6ad134215569aabb84d77694ec46a8a619abf66 gencode/java/udmi/schema/DiscoveryEvents.java +c3ff2e775880e6ec1965d74db5ef85bd640cf543e06226090b7a3f3fd83a3491 gencode/java/udmi/schema/DiscoveryEvents.java af16026c143a592562b77012dd739c02364bd1a9d6bbd6c7ede4f3a5a38b7f4a gencode/java/udmi/schema/DiscoveryModel.java 0fd348086bfd8e38a54a06b0fd205f6d01fdc6236f07ccc8bef49a1832f8c767 gencode/java/udmi/schema/DiscoveryState.java baef666f5f58f790b73ba7bed8f60eacdd75ba3151b9abc32265954a9da84faa gencode/java/udmi/schema/Electricity.java diff --git a/gencode/docs/events.html b/gencode/docs/events.html index 3b10b44c9c..9600e0ae9e 100644 --- a/gencode/docs/events.html +++ b/gencode/docs/events.html @@ -6820,7 +6820,7 @@

/> active_countType: integer
-

Total number of records expected for this active scan generation

+

Total number of records expected for this active scan generation (only populated on the event_no=0 start marker event)

@@ -6861,7 +6861,7 @@

/> addrsType: array of string
-

List of target addresses for a partial discovery scan (shard)

+

List of target addresses for a partial discovery scan shard (only populated on the event_no=0 start marker event)

diff --git a/gencode/docs/events_discovery.html b/gencode/docs/events_discovery.html index 5c049eab2a..603ade8b25 100644 --- a/gencode/docs/events_discovery.html +++ b/gencode/docs/events_discovery.html @@ -2476,7 +2476,7 @@

/> active_countType: integer
-

Total number of records expected for this active scan generation

+

Total number of records expected for this active scan generation (only populated on the event_no=0 start marker event)

@@ -2510,7 +2510,7 @@

/> addrsType: array of string
-

List of target addresses for a partial discovery scan (shard)

+

List of target addresses for a partial discovery scan shard (only populated on the event_no=0 start marker event)

@@ -47305,7 +47305,7 @@

/> active_countType: integer
-

Total number of records expected for this active scan generation

+

Total number of records expected for this active scan generation (only populated on the event_no=0 start marker event)

@@ -47360,7 +47360,7 @@

/> addrsType: array of string
-

List of target addresses for a partial discovery scan (shard)

+

List of target addresses for a partial discovery scan shard (only populated on the event_no=0 start marker event)

diff --git a/gencode/docs/metadata.html b/gencode/docs/metadata.html index e17a11d7df..4f0afe9f41 100644 --- a/gencode/docs/metadata.html +++ b/gencode/docs/metadata.html @@ -25116,7 +25116,7 @@

/> active_countType: integer
-

Total number of records expected for this active scan generation

+

Total number of records expected for this active scan generation (only populated on the event_no=0 start marker event)

@@ -25164,7 +25164,7 @@

/> addrsType: array of string
-

List of target addresses for a partial discovery scan (shard)

+

List of target addresses for a partial discovery scan shard (only populated on the event_no=0 start marker event)

diff --git a/gencode/java/udmi/schema/DiscoveryEvents.java b/gencode/java/udmi/schema/DiscoveryEvents.java index f3aa79c1b4..808a4368e7 100644 --- a/gencode/java/udmi/schema/DiscoveryEvents.java +++ b/gencode/java/udmi/schema/DiscoveryEvents.java @@ -79,18 +79,18 @@ public class DiscoveryEvents { @JsonPropertyDescription("The active or passive series number of this result (matches reported state values)") public Integer event_no; /** - * Total number of records expected for this active scan generation + * Total number of records expected for this active scan generation (only populated on the event_no=0 start marker event) * */ @JsonProperty("active_count") - @JsonPropertyDescription("Total number of records expected for this active scan generation") + @JsonPropertyDescription("Total number of records expected for this active scan generation (only populated on the event_no=0 start marker event)") public Integer active_count; /** - * List of target addresses for a partial discovery scan (shard) + * List of target addresses for a partial discovery scan shard (only populated on the event_no=0 start marker event) * */ @JsonProperty("addrs") - @JsonPropertyDescription("List of target addresses for a partial discovery scan (shard)") + @JsonPropertyDescription("List of target addresses for a partial discovery scan shard (only populated on the event_no=0 start marker event)") public List addrs; /** * Links to other address families (family and id) diff --git a/schema/events_discovery.json b/schema/events_discovery.json index 5d56afac39..857de42dcb 100644 --- a/schema/events_discovery.json +++ b/schema/events_discovery.json @@ -46,12 +46,12 @@ "type": "integer" }, "active_count": { - "description": "Total number of records expected for this active scan generation", + "description": "Total number of records expected for this active scan generation (only populated on the event_no=0 start marker event)", "minimum": 0, "type": "integer" }, "addrs": { - "description": "List of target addresses for a partial discovery scan (shard)", + "description": "List of target addresses for a partial discovery scan shard (only populated on the event_no=0 start marker event)", "type": "array", "items": { "type": "string"