forked from jappel/leistungsbilanz-ts
Add Revit Dynamo diagnostic scripts
This commit is contained in:
parent
906aa751c7
commit
204499d7e4
4 changed files with 972 additions and 0 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -4,3 +4,4 @@ dist/
|
||||||
data/*.db
|
data/*.db
|
||||||
data/backups/*.db
|
data/backups/*.db
|
||||||
.codex/*.log
|
.codex/*.log
|
||||||
|
dynamo/output/
|
||||||
|
|
|
||||||
385
dynamo/01_check_model_identity.py
Normal file
385
dynamo/01_check_model_identity.py
Normal file
|
|
@ -0,0 +1,385 @@
|
||||||
|
"""Read-only Revit 2026 model-identity diagnostics for a Dynamo Python node.
|
||||||
|
|
||||||
|
Optional Dynamo input:
|
||||||
|
IN[0]: output directory or complete .json file path
|
||||||
|
|
||||||
|
The script intentionally performs no Revit transaction and changes no model
|
||||||
|
data. OUT contains a compact summary plus the complete report.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import platform
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
import clr
|
||||||
|
|
||||||
|
clr.AddReference("RevitAPI")
|
||||||
|
clr.AddReference("RevitServices")
|
||||||
|
|
||||||
|
from Autodesk.Revit.DB import ModelPathUtils, StorageType # noqa: E402
|
||||||
|
from RevitServices.Persistence import DocumentManager # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
CHECKED_PARAMETER_NAMES = ("LB_ModelId", "LB_ProjectId")
|
||||||
|
|
||||||
|
|
||||||
|
def safe_text(value):
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return str(value)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def element_id_text(element_id):
|
||||||
|
if element_id is None:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return str(element_id.Value)
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
return str(element_id.IntegerValue)
|
||||||
|
except Exception:
|
||||||
|
return safe_text(element_id)
|
||||||
|
|
||||||
|
|
||||||
|
def forge_type_id_text(value):
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return value.TypeId
|
||||||
|
except Exception:
|
||||||
|
return safe_text(value)
|
||||||
|
|
||||||
|
|
||||||
|
def parameter_value(parameter):
|
||||||
|
result = {
|
||||||
|
"hasValue": False,
|
||||||
|
"raw": None,
|
||||||
|
"display": None,
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
result["hasValue"] = bool(parameter.HasValue)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
storage_type = parameter.StorageType
|
||||||
|
if storage_type == StorageType.String:
|
||||||
|
result["raw"] = parameter.AsString()
|
||||||
|
elif storage_type == StorageType.Integer:
|
||||||
|
result["raw"] = int(parameter.AsInteger())
|
||||||
|
elif storage_type == StorageType.Double:
|
||||||
|
result["raw"] = float(parameter.AsDouble())
|
||||||
|
elif storage_type == StorageType.ElementId:
|
||||||
|
result["raw"] = element_id_text(parameter.AsElementId())
|
||||||
|
except Exception as error:
|
||||||
|
result["readError"] = safe_text(error)
|
||||||
|
|
||||||
|
try:
|
||||||
|
result["display"] = parameter.AsValueString()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def describe_parameter(parameter):
|
||||||
|
definition = None
|
||||||
|
try:
|
||||||
|
definition = parameter.Definition
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
name = None
|
||||||
|
if definition is not None:
|
||||||
|
try:
|
||||||
|
name = definition.Name
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
is_shared = False
|
||||||
|
try:
|
||||||
|
is_shared = bool(parameter.IsShared)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
shared_guid = None
|
||||||
|
if is_shared:
|
||||||
|
try:
|
||||||
|
shared_guid = str(parameter.GUID)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
data_type = None
|
||||||
|
group_type = None
|
||||||
|
if definition is not None:
|
||||||
|
try:
|
||||||
|
data_type = forge_type_id_text(definition.GetDataType())
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
group_type = forge_type_id_text(definition.GetGroupTypeId())
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
unit_type = None
|
||||||
|
try:
|
||||||
|
unit_type = forge_type_id_text(parameter.GetUnitTypeId())
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
storage_type = str(parameter.StorageType)
|
||||||
|
except Exception:
|
||||||
|
storage_type = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
is_read_only = bool(parameter.IsReadOnly)
|
||||||
|
except Exception:
|
||||||
|
is_read_only = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
user_modifiable = bool(parameter.UserModifiable)
|
||||||
|
except Exception:
|
||||||
|
user_modifiable = None
|
||||||
|
|
||||||
|
return {
|
||||||
|
"name": name,
|
||||||
|
"parameterId": element_id_text(getattr(parameter, "Id", None)),
|
||||||
|
"isShared": is_shared,
|
||||||
|
"sharedGuid": shared_guid,
|
||||||
|
"storageType": storage_type,
|
||||||
|
"dataTypeId": data_type,
|
||||||
|
"groupTypeId": group_type,
|
||||||
|
"unitTypeId": unit_type,
|
||||||
|
"isReadOnly": is_read_only,
|
||||||
|
"userModifiable": user_modifiable,
|
||||||
|
"value": parameter_value(parameter),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def sorted_parameters(element):
|
||||||
|
parameters = []
|
||||||
|
try:
|
||||||
|
parameters = [describe_parameter(parameter) for parameter in element.Parameters]
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
return sorted(
|
||||||
|
parameters,
|
||||||
|
key=lambda parameter: (
|
||||||
|
(parameter.get("name") or "").casefold(),
|
||||||
|
parameter.get("parameterId") or "",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def named_parameter_occurrences(element, parameter_name):
|
||||||
|
result = []
|
||||||
|
try:
|
||||||
|
parameters = element.GetParameters(parameter_name)
|
||||||
|
if parameters is not None:
|
||||||
|
result = [describe_parameter(parameter) for parameter in parameters]
|
||||||
|
except Exception:
|
||||||
|
parameter = None
|
||||||
|
try:
|
||||||
|
parameter = element.LookupParameter(parameter_name)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if parameter is not None:
|
||||||
|
result = [describe_parameter(parameter)]
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def loaded_assembly_versions():
|
||||||
|
result = {}
|
||||||
|
try:
|
||||||
|
from System import AppDomain
|
||||||
|
|
||||||
|
for assembly in AppDomain.CurrentDomain.GetAssemblies():
|
||||||
|
try:
|
||||||
|
name = assembly.GetName()
|
||||||
|
simple_name = str(name.Name)
|
||||||
|
if simple_name in (
|
||||||
|
"DynamoCore",
|
||||||
|
"DynamoCoreWpf",
|
||||||
|
"DynamoRevitDS",
|
||||||
|
"RevitAPI",
|
||||||
|
"RevitServices",
|
||||||
|
):
|
||||||
|
result[simple_name] = str(name.Version)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return dict(sorted(result.items()))
|
||||||
|
|
||||||
|
|
||||||
|
def get_cloud_identity(document):
|
||||||
|
result = {"isModelInCloud": False}
|
||||||
|
try:
|
||||||
|
result["isModelInCloud"] = bool(document.IsModelInCloud)
|
||||||
|
except Exception:
|
||||||
|
return result
|
||||||
|
if not result["isModelInCloud"]:
|
||||||
|
return result
|
||||||
|
|
||||||
|
try:
|
||||||
|
model_path = document.GetCloudModelPath()
|
||||||
|
result["userVisiblePath"] = ModelPathUtils.ConvertModelPathToUserVisiblePath(
|
||||||
|
model_path
|
||||||
|
)
|
||||||
|
for property_name, output_name in (
|
||||||
|
("GetProjectGUID", "projectGuid"),
|
||||||
|
("GetModelGUID", "modelGuid"),
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
result[output_name] = str(getattr(model_path, property_name)())
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
except Exception as error:
|
||||||
|
result["readError"] = safe_text(error)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def get_worksharing_identity(document):
|
||||||
|
result = {"isWorkshared": False}
|
||||||
|
try:
|
||||||
|
result["isWorkshared"] = bool(document.IsWorkshared)
|
||||||
|
except Exception:
|
||||||
|
return result
|
||||||
|
if not result["isWorkshared"]:
|
||||||
|
return result
|
||||||
|
|
||||||
|
try:
|
||||||
|
model_path = document.GetWorksharingCentralModelPath()
|
||||||
|
result["centralModelPath"] = ModelPathUtils.ConvertModelPathToUserVisiblePath(
|
||||||
|
model_path
|
||||||
|
)
|
||||||
|
except Exception as error:
|
||||||
|
result["readError"] = safe_text(error)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_output_path(configured_path, report_name):
|
||||||
|
timestamp = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
|
||||||
|
default_directory = os.path.join(tempfile.gettempdir(), "leistungsbilanz-dynamo")
|
||||||
|
raw_path = safe_text(configured_path)
|
||||||
|
if raw_path is None or not raw_path.strip():
|
||||||
|
directory = default_directory
|
||||||
|
file_path = os.path.join(directory, report_name + "-" + timestamp + ".json")
|
||||||
|
else:
|
||||||
|
expanded = os.path.abspath(os.path.expandvars(os.path.expanduser(raw_path.strip())))
|
||||||
|
if expanded.lower().endswith(".json"):
|
||||||
|
file_path = expanded
|
||||||
|
directory = os.path.dirname(file_path)
|
||||||
|
else:
|
||||||
|
directory = expanded
|
||||||
|
file_path = os.path.join(directory, report_name + "-" + timestamp + ".json")
|
||||||
|
if not directory:
|
||||||
|
directory = os.getcwd()
|
||||||
|
if not os.path.isdir(directory):
|
||||||
|
os.makedirs(directory)
|
||||||
|
return file_path
|
||||||
|
|
||||||
|
|
||||||
|
def write_json(file_path, payload):
|
||||||
|
temporary_path = file_path + ".tmp"
|
||||||
|
with open(temporary_path, "w", encoding="utf-8", newline="\n") as output:
|
||||||
|
json.dump(payload, output, ensure_ascii=False, indent=2, sort_keys=True)
|
||||||
|
output.write("\n")
|
||||||
|
os.replace(temporary_path, file_path)
|
||||||
|
|
||||||
|
|
||||||
|
def get_input(index, default=None):
|
||||||
|
values = globals().get("IN", [])
|
||||||
|
try:
|
||||||
|
value = values[index]
|
||||||
|
return default if value is None else value
|
||||||
|
except Exception:
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
def build_report():
|
||||||
|
document = DocumentManager.Instance.CurrentDBDocument
|
||||||
|
if document is None:
|
||||||
|
raise RuntimeError("No active Revit document is available.")
|
||||||
|
|
||||||
|
project_information = document.ProjectInformation
|
||||||
|
if project_information is None:
|
||||||
|
raise RuntimeError("The active document has no Project Information element.")
|
||||||
|
|
||||||
|
application = document.Application
|
||||||
|
checked_parameters = {
|
||||||
|
name: named_parameter_occurrences(project_information, name)
|
||||||
|
for name in CHECKED_PARAMETER_NAMES
|
||||||
|
}
|
||||||
|
warnings = []
|
||||||
|
for name in CHECKED_PARAMETER_NAMES:
|
||||||
|
occurrences = checked_parameters[name]
|
||||||
|
populated = [
|
||||||
|
parameter
|
||||||
|
for parameter in occurrences
|
||||||
|
if parameter.get("value", {}).get("raw") not in (None, "")
|
||||||
|
]
|
||||||
|
if not occurrences:
|
||||||
|
warnings.append(name + " is not bound to Project Information.")
|
||||||
|
elif not populated:
|
||||||
|
warnings.append(name + " exists but has no value on Project Information.")
|
||||||
|
elif len(occurrences) > 1:
|
||||||
|
warnings.append(name + " occurs more than once; use a shared-parameter GUID later.")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"reportSchemaVersion": 1,
|
||||||
|
"generatedAt": datetime.datetime.now(datetime.timezone.utc).isoformat(),
|
||||||
|
"readOnly": True,
|
||||||
|
"environment": {
|
||||||
|
"revitVersionNumber": safe_text(getattr(application, "VersionNumber", None)),
|
||||||
|
"revitVersionName": safe_text(getattr(application, "VersionName", None)),
|
||||||
|
"revitSubVersionNumber": safe_text(
|
||||||
|
getattr(application, "SubVersionNumber", None)
|
||||||
|
),
|
||||||
|
"pythonImplementation": safe_text(getattr(sys.implementation, "name", None)),
|
||||||
|
"pythonVersion": platform.python_version(),
|
||||||
|
"assemblies": loaded_assembly_versions(),
|
||||||
|
},
|
||||||
|
"document": {
|
||||||
|
"title": safe_text(document.Title),
|
||||||
|
"pathName": safe_text(document.PathName),
|
||||||
|
"isFamilyDocument": bool(document.IsFamilyDocument),
|
||||||
|
"cloud": get_cloud_identity(document),
|
||||||
|
"worksharing": get_worksharing_identity(document),
|
||||||
|
},
|
||||||
|
"modelIdentityCandidates": {
|
||||||
|
"projectInformationUniqueId": safe_text(project_information.UniqueId),
|
||||||
|
"projectInformationElementId": element_id_text(project_information.Id),
|
||||||
|
"checkedProjectParameters": checked_parameters,
|
||||||
|
},
|
||||||
|
"projectInformationParameters": sorted_parameters(project_information),
|
||||||
|
"warnings": warnings,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
report = build_report()
|
||||||
|
output_path = resolve_output_path(get_input(0), "model-identity")
|
||||||
|
write_json(output_path, report)
|
||||||
|
OUT = {
|
||||||
|
"ok": True,
|
||||||
|
"filePath": output_path,
|
||||||
|
"projectInformationUniqueId": report["modelIdentityCandidates"][
|
||||||
|
"projectInformationUniqueId"
|
||||||
|
],
|
||||||
|
"warnings": report["warnings"],
|
||||||
|
"report": report,
|
||||||
|
}
|
||||||
|
except Exception as error:
|
||||||
|
OUT = {
|
||||||
|
"ok": False,
|
||||||
|
"error": safe_text(error),
|
||||||
|
"traceback": traceback.format_exc(),
|
||||||
|
}
|
||||||
517
dynamo/02_export_electrical_fixture_parameter_inventory.py
Normal file
517
dynamo/02_export_electrical_fixture_parameter_inventory.py
Normal file
|
|
@ -0,0 +1,517 @@
|
||||||
|
"""Export all Electrical Fixtures instance/type parameters from Revit 2026.
|
||||||
|
|
||||||
|
Optional Dynamo inputs:
|
||||||
|
IN[0]: output directory or complete .json file path
|
||||||
|
IN[1]: include empty parameters (default True)
|
||||||
|
IN[2]: maximum aggregated sample values (default 5)
|
||||||
|
|
||||||
|
The script is read-only and performs no Revit transaction.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import platform
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
import clr
|
||||||
|
|
||||||
|
clr.AddReference("RevitAPI")
|
||||||
|
clr.AddReference("RevitServices")
|
||||||
|
|
||||||
|
from Autodesk.Revit.DB import ( # noqa: E402
|
||||||
|
BuiltInCategory,
|
||||||
|
FilteredElementCollector,
|
||||||
|
ModelPathUtils,
|
||||||
|
StorageType,
|
||||||
|
)
|
||||||
|
from RevitServices.Persistence import DocumentManager # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
def safe_text(value):
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return str(value)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def element_id_text(element_id):
|
||||||
|
if element_id is None:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return str(element_id.Value)
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
return str(element_id.IntegerValue)
|
||||||
|
except Exception:
|
||||||
|
return safe_text(element_id)
|
||||||
|
|
||||||
|
|
||||||
|
def forge_type_id_text(value):
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return value.TypeId
|
||||||
|
except Exception:
|
||||||
|
return safe_text(value)
|
||||||
|
|
||||||
|
|
||||||
|
def parameter_value(parameter):
|
||||||
|
result = {"hasValue": False, "raw": None, "display": None}
|
||||||
|
try:
|
||||||
|
result["hasValue"] = bool(parameter.HasValue)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
storage_type = parameter.StorageType
|
||||||
|
if storage_type == StorageType.String:
|
||||||
|
result["raw"] = parameter.AsString()
|
||||||
|
elif storage_type == StorageType.Integer:
|
||||||
|
result["raw"] = int(parameter.AsInteger())
|
||||||
|
elif storage_type == StorageType.Double:
|
||||||
|
result["raw"] = float(parameter.AsDouble())
|
||||||
|
elif storage_type == StorageType.ElementId:
|
||||||
|
result["raw"] = element_id_text(parameter.AsElementId())
|
||||||
|
except Exception as error:
|
||||||
|
result["readError"] = safe_text(error)
|
||||||
|
|
||||||
|
try:
|
||||||
|
result["display"] = parameter.AsValueString()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def describe_parameter(parameter, scope):
|
||||||
|
definition = None
|
||||||
|
try:
|
||||||
|
definition = parameter.Definition
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
name = None
|
||||||
|
data_type = None
|
||||||
|
group_type = None
|
||||||
|
if definition is not None:
|
||||||
|
try:
|
||||||
|
name = definition.Name
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
data_type = forge_type_id_text(definition.GetDataType())
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
group_type = forge_type_id_text(definition.GetGroupTypeId())
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
is_shared = False
|
||||||
|
try:
|
||||||
|
is_shared = bool(parameter.IsShared)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
shared_guid = None
|
||||||
|
if is_shared:
|
||||||
|
try:
|
||||||
|
shared_guid = str(parameter.GUID)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
unit_type = None
|
||||||
|
try:
|
||||||
|
unit_type = forge_type_id_text(parameter.GetUnitTypeId())
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
storage_type = str(parameter.StorageType)
|
||||||
|
except Exception:
|
||||||
|
storage_type = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
is_read_only = bool(parameter.IsReadOnly)
|
||||||
|
except Exception:
|
||||||
|
is_read_only = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
user_modifiable = bool(parameter.UserModifiable)
|
||||||
|
except Exception:
|
||||||
|
user_modifiable = None
|
||||||
|
|
||||||
|
return {
|
||||||
|
"scope": scope,
|
||||||
|
"name": name,
|
||||||
|
"parameterId": element_id_text(getattr(parameter, "Id", None)),
|
||||||
|
"isShared": is_shared,
|
||||||
|
"sharedGuid": shared_guid,
|
||||||
|
"storageType": storage_type,
|
||||||
|
"dataTypeId": data_type,
|
||||||
|
"groupTypeId": group_type,
|
||||||
|
"unitTypeId": unit_type,
|
||||||
|
"isReadOnly": is_read_only,
|
||||||
|
"userModifiable": user_modifiable,
|
||||||
|
"value": parameter_value(parameter),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def has_meaningful_value(parameter_description):
|
||||||
|
value = parameter_description.get("value", {})
|
||||||
|
return bool(value.get("hasValue")) or value.get("raw") not in (None, "") or value.get(
|
||||||
|
"display"
|
||||||
|
) not in (None, "")
|
||||||
|
|
||||||
|
|
||||||
|
def read_parameters(element, scope, include_empty):
|
||||||
|
result = []
|
||||||
|
try:
|
||||||
|
for parameter in element.Parameters:
|
||||||
|
description = describe_parameter(parameter, scope)
|
||||||
|
if include_empty or has_meaningful_value(description):
|
||||||
|
result.append(description)
|
||||||
|
except Exception as error:
|
||||||
|
return [], [safe_text(error)]
|
||||||
|
result.sort(
|
||||||
|
key=lambda parameter: (
|
||||||
|
(parameter.get("name") or "").casefold(),
|
||||||
|
parameter.get("parameterId") or "",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return result, []
|
||||||
|
|
||||||
|
|
||||||
|
def read_space(element, document):
|
||||||
|
try:
|
||||||
|
space = element.Space
|
||||||
|
except Exception as error:
|
||||||
|
return None, safe_text(error)
|
||||||
|
if space is None:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
level_name = None
|
||||||
|
try:
|
||||||
|
level = document.GetElement(space.LevelId)
|
||||||
|
level_name = None if level is None else safe_text(level.Name)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return {
|
||||||
|
"uniqueId": safe_text(space.UniqueId),
|
||||||
|
"elementId": element_id_text(space.Id),
|
||||||
|
"number": safe_text(getattr(space, "Number", None)),
|
||||||
|
"name": safe_text(getattr(space, "Name", None)),
|
||||||
|
"levelName": level_name,
|
||||||
|
}, None
|
||||||
|
|
||||||
|
|
||||||
|
def read_family_identity(element, document):
|
||||||
|
symbol = None
|
||||||
|
try:
|
||||||
|
symbol = element.Symbol
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
symbol = document.GetElement(element.GetTypeId())
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
family_name = None
|
||||||
|
type_name = None
|
||||||
|
type_unique_id = None
|
||||||
|
type_element_id = None
|
||||||
|
if symbol is not None:
|
||||||
|
try:
|
||||||
|
family_name = safe_text(symbol.Family.Name)
|
||||||
|
except Exception:
|
||||||
|
family_name = safe_text(getattr(symbol, "FamilyName", None))
|
||||||
|
type_name = safe_text(getattr(symbol, "Name", None))
|
||||||
|
type_unique_id = safe_text(getattr(symbol, "UniqueId", None))
|
||||||
|
type_element_id = element_id_text(getattr(symbol, "Id", None))
|
||||||
|
|
||||||
|
return {
|
||||||
|
"familyName": family_name,
|
||||||
|
"typeName": type_name,
|
||||||
|
"typeUniqueId": type_unique_id,
|
||||||
|
"typeElementId": type_element_id,
|
||||||
|
}, symbol
|
||||||
|
|
||||||
|
|
||||||
|
def parameter_inventory_key(parameter):
|
||||||
|
stable_id = parameter.get("sharedGuid") or parameter.get("parameterId") or ""
|
||||||
|
return "|".join(
|
||||||
|
(
|
||||||
|
parameter.get("scope") or "",
|
||||||
|
stable_id,
|
||||||
|
parameter.get("name") or "",
|
||||||
|
parameter.get("dataTypeId") or "",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def sample_value_key(value):
|
||||||
|
return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
||||||
|
|
||||||
|
|
||||||
|
def add_to_inventory(inventory, parameter, max_samples):
|
||||||
|
key = parameter_inventory_key(parameter)
|
||||||
|
entry = inventory.get(key)
|
||||||
|
if entry is None:
|
||||||
|
entry = {
|
||||||
|
"scope": parameter.get("scope"),
|
||||||
|
"name": parameter.get("name"),
|
||||||
|
"parameterId": parameter.get("parameterId"),
|
||||||
|
"isShared": parameter.get("isShared"),
|
||||||
|
"sharedGuid": parameter.get("sharedGuid"),
|
||||||
|
"storageType": parameter.get("storageType"),
|
||||||
|
"dataTypeId": parameter.get("dataTypeId"),
|
||||||
|
"groupTypeId": parameter.get("groupTypeId"),
|
||||||
|
"unitTypeId": parameter.get("unitTypeId"),
|
||||||
|
"occurrenceCount": 0,
|
||||||
|
"populatedCount": 0,
|
||||||
|
"sampleValues": [],
|
||||||
|
"_sampleKeys": set(),
|
||||||
|
}
|
||||||
|
inventory[key] = entry
|
||||||
|
entry["occurrenceCount"] += 1
|
||||||
|
if has_meaningful_value(parameter):
|
||||||
|
entry["populatedCount"] += 1
|
||||||
|
value = parameter.get("value")
|
||||||
|
value_key = sample_value_key(value)
|
||||||
|
if len(entry["sampleValues"]) < max_samples and value_key not in entry["_sampleKeys"]:
|
||||||
|
entry["_sampleKeys"].add(value_key)
|
||||||
|
entry["sampleValues"].append(value)
|
||||||
|
|
||||||
|
|
||||||
|
def finalize_inventory(inventory):
|
||||||
|
result = []
|
||||||
|
for entry in inventory.values():
|
||||||
|
clean_entry = dict(entry)
|
||||||
|
clean_entry.pop("_sampleKeys", None)
|
||||||
|
result.append(clean_entry)
|
||||||
|
return sorted(
|
||||||
|
result,
|
||||||
|
key=lambda entry: (
|
||||||
|
entry.get("scope") or "",
|
||||||
|
(entry.get("name") or "").casefold(),
|
||||||
|
entry.get("sharedGuid") or entry.get("parameterId") or "",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def loaded_assembly_versions():
|
||||||
|
result = {}
|
||||||
|
try:
|
||||||
|
from System import AppDomain
|
||||||
|
|
||||||
|
for assembly in AppDomain.CurrentDomain.GetAssemblies():
|
||||||
|
try:
|
||||||
|
name = assembly.GetName()
|
||||||
|
simple_name = str(name.Name)
|
||||||
|
if simple_name in (
|
||||||
|
"DynamoCore",
|
||||||
|
"DynamoCoreWpf",
|
||||||
|
"DynamoRevitDS",
|
||||||
|
"RevitAPI",
|
||||||
|
"RevitServices",
|
||||||
|
):
|
||||||
|
result[simple_name] = str(name.Version)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return dict(sorted(result.items()))
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_output_path(configured_path):
|
||||||
|
timestamp = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
|
||||||
|
default_directory = os.path.join(tempfile.gettempdir(), "leistungsbilanz-dynamo")
|
||||||
|
raw_path = safe_text(configured_path)
|
||||||
|
if raw_path is None or not raw_path.strip():
|
||||||
|
directory = default_directory
|
||||||
|
file_path = os.path.join(
|
||||||
|
directory, "electrical-fixture-parameters-" + timestamp + ".json"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
expanded = os.path.abspath(os.path.expandvars(os.path.expanduser(raw_path.strip())))
|
||||||
|
if expanded.lower().endswith(".json"):
|
||||||
|
file_path = expanded
|
||||||
|
directory = os.path.dirname(file_path)
|
||||||
|
else:
|
||||||
|
directory = expanded
|
||||||
|
file_path = os.path.join(
|
||||||
|
directory, "electrical-fixture-parameters-" + timestamp + ".json"
|
||||||
|
)
|
||||||
|
if not directory:
|
||||||
|
directory = os.getcwd()
|
||||||
|
if not os.path.isdir(directory):
|
||||||
|
os.makedirs(directory)
|
||||||
|
return file_path
|
||||||
|
|
||||||
|
|
||||||
|
def write_json(file_path, payload):
|
||||||
|
temporary_path = file_path + ".tmp"
|
||||||
|
with open(temporary_path, "w", encoding="utf-8", newline="\n") as output:
|
||||||
|
json.dump(payload, output, ensure_ascii=False, indent=2, sort_keys=True)
|
||||||
|
output.write("\n")
|
||||||
|
os.replace(temporary_path, file_path)
|
||||||
|
|
||||||
|
|
||||||
|
def get_input(index, default=None):
|
||||||
|
values = globals().get("IN", [])
|
||||||
|
try:
|
||||||
|
value = values[index]
|
||||||
|
return default if value is None else value
|
||||||
|
except Exception:
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
def build_report(include_empty, max_samples):
|
||||||
|
document = DocumentManager.Instance.CurrentDBDocument
|
||||||
|
if document is None:
|
||||||
|
raise RuntimeError("No active Revit document is available.")
|
||||||
|
if document.IsFamilyDocument:
|
||||||
|
raise RuntimeError("Open a Revit project document, not a family document.")
|
||||||
|
|
||||||
|
application = document.Application
|
||||||
|
collector = (
|
||||||
|
FilteredElementCollector(document)
|
||||||
|
.OfCategory(BuiltInCategory.OST_ElectricalFixtures)
|
||||||
|
.WhereElementIsNotElementType()
|
||||||
|
)
|
||||||
|
source_elements = list(collector)
|
||||||
|
source_elements.sort(key=lambda element: safe_text(element.UniqueId) or "")
|
||||||
|
|
||||||
|
elements = []
|
||||||
|
types_by_unique_id = {}
|
||||||
|
inventory = {}
|
||||||
|
errors = []
|
||||||
|
elements_without_space = 0
|
||||||
|
|
||||||
|
for element in source_elements:
|
||||||
|
element_errors = []
|
||||||
|
try:
|
||||||
|
family_identity, symbol = read_family_identity(element, document)
|
||||||
|
instance_parameters, parameter_errors = read_parameters(
|
||||||
|
element, "instance", include_empty
|
||||||
|
)
|
||||||
|
element_errors.extend(parameter_errors)
|
||||||
|
for parameter in instance_parameters:
|
||||||
|
add_to_inventory(inventory, parameter, max_samples)
|
||||||
|
|
||||||
|
type_unique_id = family_identity.get("typeUniqueId")
|
||||||
|
if symbol is not None and type_unique_id and type_unique_id not in types_by_unique_id:
|
||||||
|
type_parameters, type_errors = read_parameters(symbol, "type", include_empty)
|
||||||
|
element_errors.extend(type_errors)
|
||||||
|
for parameter in type_parameters:
|
||||||
|
add_to_inventory(inventory, parameter, max_samples)
|
||||||
|
types_by_unique_id[type_unique_id] = {
|
||||||
|
**family_identity,
|
||||||
|
"parameters": type_parameters,
|
||||||
|
}
|
||||||
|
|
||||||
|
space, space_error = read_space(element, document)
|
||||||
|
if space_error:
|
||||||
|
element_errors.append("MEP Space: " + space_error)
|
||||||
|
if space is None:
|
||||||
|
elements_without_space += 1
|
||||||
|
|
||||||
|
elements.append(
|
||||||
|
{
|
||||||
|
"uniqueId": safe_text(element.UniqueId),
|
||||||
|
"elementId": element_id_text(element.Id),
|
||||||
|
"categoryName": safe_text(
|
||||||
|
None if element.Category is None else element.Category.Name
|
||||||
|
),
|
||||||
|
"family": family_identity,
|
||||||
|
"space": space,
|
||||||
|
"instanceParameters": instance_parameters,
|
||||||
|
"warnings": element_errors,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
except Exception as error:
|
||||||
|
errors.append(
|
||||||
|
{
|
||||||
|
"uniqueId": safe_text(getattr(element, "UniqueId", None)),
|
||||||
|
"elementId": element_id_text(getattr(element, "Id", None)),
|
||||||
|
"error": safe_text(error),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"reportSchemaVersion": 1,
|
||||||
|
"generatedAt": datetime.datetime.now(datetime.timezone.utc).isoformat(),
|
||||||
|
"readOnly": True,
|
||||||
|
"complete": len(errors) == 0,
|
||||||
|
"scope": {
|
||||||
|
"builtInCategory": "OST_ElectricalFixtures",
|
||||||
|
"wholeDocument": True,
|
||||||
|
"elementTypesExcluded": True,
|
||||||
|
"includeEmptyParameters": include_empty,
|
||||||
|
},
|
||||||
|
"environment": {
|
||||||
|
"revitVersionNumber": safe_text(getattr(application, "VersionNumber", None)),
|
||||||
|
"revitVersionName": safe_text(getattr(application, "VersionName", None)),
|
||||||
|
"revitSubVersionNumber": safe_text(
|
||||||
|
getattr(application, "SubVersionNumber", None)
|
||||||
|
),
|
||||||
|
"pythonImplementation": safe_text(getattr(sys.implementation, "name", None)),
|
||||||
|
"pythonVersion": platform.python_version(),
|
||||||
|
"assemblies": loaded_assembly_versions(),
|
||||||
|
},
|
||||||
|
"document": {
|
||||||
|
"title": safe_text(document.Title),
|
||||||
|
"pathName": safe_text(document.PathName),
|
||||||
|
"projectInformationUniqueId": safe_text(document.ProjectInformation.UniqueId),
|
||||||
|
},
|
||||||
|
"summary": {
|
||||||
|
"elementCount": len(source_elements),
|
||||||
|
"exportedElementCount": len(elements),
|
||||||
|
"typeCount": len(types_by_unique_id),
|
||||||
|
"parameterDefinitionCount": len(inventory),
|
||||||
|
"elementsWithoutMepSpace": elements_without_space,
|
||||||
|
"elementErrorCount": len(errors),
|
||||||
|
},
|
||||||
|
"parameterInventory": finalize_inventory(inventory),
|
||||||
|
"types": sorted(
|
||||||
|
types_by_unique_id.values(),
|
||||||
|
key=lambda entry: (
|
||||||
|
(entry.get("familyName") or "").casefold(),
|
||||||
|
(entry.get("typeName") or "").casefold(),
|
||||||
|
entry.get("typeUniqueId") or "",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
"elements": elements,
|
||||||
|
"errors": errors,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
include_empty_input = get_input(1, True)
|
||||||
|
include_empty = bool(include_empty_input)
|
||||||
|
try:
|
||||||
|
max_samples = int(get_input(2, 5))
|
||||||
|
except Exception:
|
||||||
|
max_samples = 5
|
||||||
|
max_samples = max(0, min(max_samples, 50))
|
||||||
|
|
||||||
|
report = build_report(include_empty, max_samples)
|
||||||
|
output_path = resolve_output_path(get_input(0))
|
||||||
|
write_json(output_path, report)
|
||||||
|
OUT = {
|
||||||
|
"ok": True,
|
||||||
|
"filePath": output_path,
|
||||||
|
"complete": report["complete"],
|
||||||
|
"summary": report["summary"],
|
||||||
|
"errors": report["errors"],
|
||||||
|
}
|
||||||
|
except Exception as error:
|
||||||
|
OUT = {
|
||||||
|
"ok": False,
|
||||||
|
"error": safe_text(error),
|
||||||
|
"traceback": traceback.format_exc(),
|
||||||
|
}
|
||||||
69
dynamo/README.md
Normal file
69
dynamo/README.md
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
# Revit 2026 / Dynamo diagnostics
|
||||||
|
|
||||||
|
This directory contains self-contained Python scripts for a Dynamo **Python
|
||||||
|
Script** node. They use only Dynamo's built-in Revit integration, the Revit API
|
||||||
|
and the Python standard library. No Dynamo package is required.
|
||||||
|
|
||||||
|
The scripts are read-only. They do not start a Revit transaction and do not
|
||||||
|
change the open model.
|
||||||
|
|
||||||
|
## Python engine
|
||||||
|
|
||||||
|
Use the built-in `CPython3` engine in Revit 2026. Autodesk ships Dynamo with
|
||||||
|
Revit; optional PythonNet3 packages are not required by these diagnostics.
|
||||||
|
|
||||||
|
## 01 - Check model identity
|
||||||
|
|
||||||
|
File: `01_check_model_identity.py`
|
||||||
|
|
||||||
|
The script reports:
|
||||||
|
|
||||||
|
- Revit, Dynamo and Python versions;
|
||||||
|
- `ProjectInformation.UniqueId` as a native model-identity candidate;
|
||||||
|
- all occurrences and values of `LB_ModelId` and `LB_ProjectId` on Project
|
||||||
|
Information;
|
||||||
|
- all Project Information parameters;
|
||||||
|
- optional cloud/worksharing identity information when the API exposes it.
|
||||||
|
|
||||||
|
Input `IN[0]` is optional. It may be either an output directory or a complete
|
||||||
|
`.json` file path. With no input, the report is written below the current
|
||||||
|
Windows temporary directory in `leistungsbilanz-dynamo`.
|
||||||
|
|
||||||
|
## 02 - Inventory Electrical Fixtures parameters
|
||||||
|
|
||||||
|
File: `02_export_electrical_fixture_parameter_inventory.py`
|
||||||
|
|
||||||
|
The script reads every instance of
|
||||||
|
`BuiltInCategory.OST_ElectricalFixtures` in the complete current document. It
|
||||||
|
exports:
|
||||||
|
|
||||||
|
- element, family, type and MEP Space identities;
|
||||||
|
- every instance parameter and value;
|
||||||
|
- every unique family-type parameter and value;
|
||||||
|
- an aggregated parameter inventory with occurrence counts and sample values;
|
||||||
|
- per-element warnings instead of aborting at the first unreadable element.
|
||||||
|
|
||||||
|
Inputs:
|
||||||
|
|
||||||
|
- `IN[0]` (optional): output directory or complete `.json` path;
|
||||||
|
- `IN[1]` (optional): include empty parameters, default `true`;
|
||||||
|
- `IN[2]` (optional): maximum sample values per aggregated parameter, default
|
||||||
|
`5`.
|
||||||
|
|
||||||
|
The default output location is again the Windows temporary directory. The
|
||||||
|
generated report can contain model paths and project-specific parameter values;
|
||||||
|
review it before sharing or committing it.
|
||||||
|
|
||||||
|
## Running a script
|
||||||
|
|
||||||
|
1. Open the target model in Revit 2026.
|
||||||
|
2. Open Dynamo from **Manage > Visual Programming > Dynamo**.
|
||||||
|
3. Create a graph and add a **Python Script** node.
|
||||||
|
4. Select the `CPython3` engine for the node.
|
||||||
|
5. Copy the complete content of the desired `.py` file into the node.
|
||||||
|
6. Optionally connect a String node containing the output path to `IN[0]`.
|
||||||
|
7. Run the graph and inspect `OUT` for status, counts and the generated path.
|
||||||
|
|
||||||
|
For the first test, run `01_check_model_identity.py` in the Revit main model.
|
||||||
|
Then run the parameter inventory. Keep both generated JSON files so their
|
||||||
|
structure can be checked before the production snapshot DTO is finalized.
|
||||||
Loading…
Add table
Add a link
Reference in a new issue