forked from jappel/leistungsbilanz-ts
385 lines
11 KiB
Python
385 lines
11 KiB
Python
"""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(),
|
|
}
|