From 7546b752930b8e89b6d02a82e1d698914456738e Mon Sep 17 00:00:00 2001 From: Quentin WEPHRE Date: Tue, 26 Aug 2025 10:31:02 +0200 Subject: [PATCH] Safe device declaration from xlsx (example file from Bell project provided) --- BELL_DEVICE_LIST_CS.xlsx.example | Bin 0 -> 11366 bytes Python/create_iot_hub_devices_from_excel.py | 157 ++++++++++++++++++++ 2 files changed, 157 insertions(+) create mode 100644 BELL_DEVICE_LIST_CS.xlsx.example create mode 100644 Python/create_iot_hub_devices_from_excel.py diff --git a/BELL_DEVICE_LIST_CS.xlsx.example b/BELL_DEVICE_LIST_CS.xlsx.example new file mode 100644 index 0000000000000000000000000000000000000000..583ef9f8ec836148c3b6065647101e959d6ca2e2 GIT binary patch literal 11366 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 str: + """ + Constructs the primary connection string for a given device object. + + :param device: The IoTHubRegistryManager Device object. + :param host_name: The IoT Hub hostname (e.g., 'your-hub.azure-devices.net'). + :return: The device connection string. + """ + if not device or not device.authentication or not device.authentication.symmetric_key: + return "Error: Could not retrieve symmetric key." + + primary_key = device.authentication.symmetric_key.primary_key + connection_string = ( + f"HostName={host_name};" + f"DeviceId={device.device_id};" + f"SharedAccessKey={primary_key}" + ) + return connection_string + +def main(): + """ + Main function to provision devices from an Excel file and save results. + """ + if not IOTHUB_CONNECTION_STRING: + print("Error: The IOTHUB_CONNECTION_STRING environment variable is not set.") + return + + try: + registry_manager = IoTHubRegistryManager.from_connection_string(IOTHUB_CONNECTION_STRING) + # Extract the hub hostname once for use in connection strings + host_name = IOTHUB_CONNECTION_STRING.split(';')[0].split('=')[1] + + df = pd.read_excel(INPUT_EXCEL_FILE) + # Add the new column to store connection strings, initialized as empty + df['connectionString'] = '' + + for index, row in df.iterrows(): + device_id = row['deviceId'] + device_type = row['deviceType'] + site = row['site'] + number = row['number'] + version = row.get('version') + + print(f"\n--- Processing Device: {device_id} ---") + + final_device_obj = None # Will hold the device object after creation/validation + + try: + # 1. CHECK IF DEVICE EXISTS + existing_device = registry_manager.get_device(device_id) + print(f"Device '{device_id}' already exists. Validating configuration...") + + # 2. VALIDATE IOT EDGE STATUS + is_edge_in_hub = existing_device.capabilities.iot_edge + is_edge_in_file = (device_type == 'AC_GATEWAY') + + if is_edge_in_hub != is_edge_in_file: + print(f" [MISMATCH] IoT Edge status is incorrect. Deleting and recreating.") + registry_manager.delete_device(device_id) + final_device_obj = create_device_from_spec(registry_manager, device_id, device_type, site, number, version) + else: + print(" [OK] IoT Edge status is correct. Updating tags...") + update_device_tags(registry_manager, device_id, device_type, site, number, version) + final_device_obj = existing_device + + except HttpOperationError as http_err: + # Check the status code on the nested 'response' object + if http_err.response.status_code == 404: + print(f"Device '{device_id}' does not exist. Creating...") + final_device_obj = create_device_from_spec(registry_manager, device_id, device_type, site, number, version) + else: + # It's a different HTTP error (e.g., 401 Unauthorized, 503 Service Unavailable) + print(f" [ERROR] An unexpected HTTP error occurred for {device_id}:") + # The response body often has the most detailed error message + if http_err.response: + print(f" Status Code: {http_err.response.status_code}") + print(f" Response Text: {http_err.response.text}") + else: + print(f" Full Error: {http_err}") + + except Exception as e: + print(f"{e.__class__} {e.__str__}") + + # 4. IF A DEVICE WAS CREATED OR VALIDATED, GET AND SAVE ITS CONNECTION STRING + if final_device_obj: + connection_string = get_connection_string(final_device_obj, host_name) + # Use .at for efficient, label-based assignment + df.at[index, 'connectionString'] = connection_string + print(f" > Connection string recorded for '{device_id}'.") + + # 5. SAVE THE UPDATED DATAFRAME TO A NEW EXCEL FILE + df.to_excel(OUTPUT_EXCEL_FILE, index=False) + print(f"\n--- Processing Complete ---") + print(f"Results, including connection strings, have been saved to '{OUTPUT_EXCEL_FILE}'.") + + except FileNotFoundError: + print(f"Error: The input file '{INPUT_EXCEL_FILE}' was not found.") + except Exception as e: + print(f"A general error occurred: {e}") + +def create_device_from_spec(registry_manager, device_id, device_type, site, number, version) -> Device: + """Creates a device, sets its tags, and returns the created device object.""" + try: + is_edge = (device_type == 'AC_GATEWAY') + + device = registry_manager.create_device_with_sas( + device_id, "", "", status="enabled", iot_edge=is_edge + ) + print(f" > Created {'Edge' if is_edge else 'non-Edge'} device '{device_id}'.") + + # After creating, update the tags + update_device_tags(registry_manager, device_id, device_type, site, number, version) + return device # Return the newly created device object + + except ResourceExistsError: + print(f" [WARNING] Device {device_id} already exists. Attempting to retrieve it.") + return registry_manager.get_device(device_id) + except Exception as e: + print(f" [ERROR] Failed to create device {device_id}: {e}") + return None + +def update_device_tags(registry_manager, device_id, device_type, site, number, version): + """Updates the device twin tags based on its type.""" + try: + tags = { + "site": site, + "number": int(number), + "deviceType": device_type + } + if device_type == 'AC_GATEWAY': + tags["version"] = str(version) + + twin = registry_manager.get_twin(device_id) + twin_patch = Twin(tags=tags) + registry_manager.update_twin(device_id, twin_patch, twin.etag) + print(f" > Successfully updated tags for '{device_id}'.") + + except Exception as e: + print(f" [ERROR] Failed to update tags for {device_id}: {e}") + +if __name__ == '__main__': + main() \ No newline at end of file