def main(self):

        # Create a selection
        sel = self.uidoc.Selection
        # Prompt the user to pick the required external link
        try:
            ref = sel.PickObject(ObjectType.Element,
                                 "Please pick a linked model instance")

            # Get AR RVT link
            rvt_link = self.doc.GetElement(ref.ElementId)
            if not self.rvt_link_check(rvt_link):
                WinForms.MessageBox.Show("The operation was cancelled!",
                                         "Error!",
                                         WinForms.MessageBoxButtons.OK,
                                         WinForms.MessageBoxIcon.Information)
                return

            self.transform = rvt_link.GetTotalTransform()
            linkedDoc = rvt_link.GetLinkDocument()

            # Create a room collector instance
            room_collector = FilteredElementCollector(linkedDoc)

            # Create a space collector instance
            self._space_collector = FilteredElementCollector(
                self.doc).WhereElementIsNotElementType().OfCategory(
                    BuiltInCategory.OST_MEPSpaces)

            # Collect levels from the current document
            levels = FilteredElementCollector(
                self.doc).WhereElementIsNotElementType().OfCategory(
                    BuiltInCategory.OST_Levels)

            # For each level in the current model define its elevation and create level elevation:Level dictionary
            self.lvls_dict = {level.Elevation: level for level in levels}

            # Collect rooms from RVT link
            if room_collector and room_collector.GetElementCount() != 0:
                self.rooms_list = room_collector.WhereElementIsNotElementType(
                ).OfCategory(BuiltInCategory.OST_Rooms)

            self.counter = 0
            self.startProgress.emit(room_collector.GetElementCount() + self.spaces_count + \
                (2*(room_collector.GetElementCount()*len(self._excel_parameters))))
            self.__main()
            self.endProgress.emit()

        except Exception as e:
            logger.error(e, exc_info=True)
            WinForms.MessageBox.Show("The operation was cancelled!", "Error!",
                                     WinForms.MessageBoxButtons.OK,
                                     WinForms.MessageBoxIcon.Information)
            return
class Model(object):
    def __init__(self,
                 __revit__,
                 search_id=None,
                 excel_parameters=None,
                 xl_file_path=None,
                 xl_write_flag=False):
        # region Get Document and Application
        self.doc = __revit__.ActiveUIDocument.Document
        self.uidoc = UIDocument(self.doc)
        self.app = __revit__.Application
        self.uiapp = UIApplication(self.app)
        self.excel = Excel.ApplicationClass()
        System.Threading.Thread.CurrentThread.CurrentCulture = System.Globalization.CultureInfo(
            "en-US")
        # endregion

        # region Initial parameters
        self._search_id = search_id
        self._excel_parameters = excel_parameters
        self._xl_file_path = xl_file_path
        self._xl_write_flag = xl_write_flag
        self.New_MEPSpaces = {}
        self.Exist_MEPSpaces = {}

        # Create a space collector instance
        self._space_collector = FilteredElementCollector(
            self.doc).WhereElementIsNotElementType().OfCategory(
                BuiltInCategory.OST_MEPSpaces)

        # endregion

        # region Custom Events
        self.startProgress = Event()
        self.ReportProgress = Event()
        self.endProgress = Event()

        # endregion

    # region Getters and Setters
    @property
    def search_id(self):
        return self._search_id

    @search_id.setter
    def search_id(self, value):
        self._search_id = value

    @property
    def xl_file_path(self):
        return self._xl_file_path

    @xl_file_path.setter
    def xl_file_path(self, value):
        self._xl_file_path = value

    @property
    def xl_write_flag(self):
        return self._xl_write_flag

    @xl_write_flag.setter
    def xl_write_flag(self, value):
        self._xl_write_flag = value

    @property
    def excel_parameters(self):
        return self._excel_parameters

    @excel_parameters.setter
    def excel_parameters(self, par_list):
        self._excel_parameters = par_list

    @property
    def spaces_count(self):
        self._spaces_count = self._space_collector.GetElementCount()
        return self._spaces_count

    # endregion Getters and Setters

    def main(self):

        # Create a selection
        sel = self.uidoc.Selection
        # Prompt the user to pick the required external link
        try:
            ref = sel.PickObject(ObjectType.Element,
                                 "Please pick a linked model instance")

            # Get AR RVT link
            rvt_link = self.doc.GetElement(ref.ElementId)
            if not self.rvt_link_check(rvt_link):
                WinForms.MessageBox.Show("The operation was cancelled!",
                                         "Error!",
                                         WinForms.MessageBoxButtons.OK,
                                         WinForms.MessageBoxIcon.Information)
                return

            self.transform = rvt_link.GetTotalTransform()
            linkedDoc = rvt_link.GetLinkDocument()

            # Create a room collector instance
            room_collector = FilteredElementCollector(linkedDoc)

            # Create a space collector instance
            self._space_collector = FilteredElementCollector(
                self.doc).WhereElementIsNotElementType().OfCategory(
                    BuiltInCategory.OST_MEPSpaces)

            # Collect levels from the current document
            levels = FilteredElementCollector(
                self.doc).WhereElementIsNotElementType().OfCategory(
                    BuiltInCategory.OST_Levels)

            # For each level in the current model define its elevation and create level elevation:Level dictionary
            self.lvls_dict = {level.Elevation: level for level in levels}

            # Collect rooms from RVT link
            if room_collector and room_collector.GetElementCount() != 0:
                self.rooms_list = room_collector.WhereElementIsNotElementType(
                ).OfCategory(BuiltInCategory.OST_Rooms)

            self.counter = 0
            self.startProgress.emit(room_collector.GetElementCount() + self.spaces_count + \
                (2*(room_collector.GetElementCount()*len(self._excel_parameters))))
            self.__main()
            self.endProgress.emit()

        except Exception as e:
            logger.error(e, exc_info=True)
            WinForms.MessageBox.Show("The operation was cancelled!", "Error!",
                                     WinForms.MessageBoxButtons.OK,
                                     WinForms.MessageBoxIcon.Information)
            return

    def __main(self):
        try:
            # Create spaces by rooms
            self.create_spaces_by_rooms(self.rooms_list)

            # Write data to an excel file
            if self._xl_write_flag:
                self.__write_to_excel(self._excel_parameters)

        except Exception as e:
            logger.error(e, exc_info=True)
            pass

    def rvt_link_check(self, rvt_link_instance):
        rvt_link_type = self.doc.GetElement(rvt_link_instance.GetTypeId())
        try:
            room_bound = rvt_link_type.get_Parameter(
                BuiltInParameter.WALL_ATTR_ROOM_BOUNDING)
            if not room_bound.AsInteger():
                result = WinForms.MessageBox.Show(
                    "Room Bounding is turned off! Would you like to turn it on?",
                    "Warning!", WinForms.MessageBoxButtons.YesNo,
                    WinForms.MessageBoxIcon.Question)

                if result == WinForms.DialogResult.Yes:
                    with Transaction(self.doc, "Set Room Bounding") as tr:
                        tr.Start()
                        room_bound.Set(1)
                        tr.Commit()
                    return True
                return False
            return True

        except Exception as e:
            logger.error(e, exc_info=True)
            return False

    def create_spaces_by_rooms(self, rooms):
        # Initiate the transacton group
        with TransactionGroup(self.doc,
                              "Batch create spaces/Transfer parameters") as tg:
            tg.Start()
            Room_UniqueIds = [
                room.UniqueId for room in rooms
                if room.Area > 0 and room.Location != None
            ]
            self.counter += 1
            self.ReportProgress.emit(self.counter)
            # Define if there're spaces in a model
            if self._space_collector.GetElementCount() == 0:
                # If there are no spaces
                # Create a space
                for room in rooms:
                    if room.Area > 0 and room.UniqueId not in self.New_MEPSpaces:
                        self.space_creator(room, self.lvls_dict)

                        self.counter += 1
                        self.ReportProgress.emit(self.counter)

            # If there are spaces in the model
            else:
                self.space_check(Room_UniqueIds)
                # For each room in RVT link rooms take room UniqueId and check if there's space with the same Id among the existing MEP spaces
                for room in rooms:
                    # If there's such space get it
                    if room.UniqueId in self.Exist_MEPSpaces:
                        exst_space = self.Exist_MEPSpaces[room.UniqueId]
                        self.space_updater(room, exst_space)

                        self.counter += 1
                        self.ReportProgress.emit(self.counter)

                    # If there's no such space
                    else:
                        # Create a space
                        if room.Area > 0 and room.UniqueId not in self.New_MEPSpaces:
                            self.space_creator(room, self.lvls_dict)

                        self.counter += 1
                        self.ReportProgress.emit(self.counter)

            tg.Assimilate()

    def space_creator(self, room, lvls):
        ''' 
        Function creates new spaces 
        room: Revit Room, lvls: level elevations and levels dictionary
        '''
        try:
            # Get the room level
            room_lvl = room.Level
            # Get a level from the lvls dictionary by room level elevation
            lvl = lvls.get(room_lvl.Elevation)
            # Create space by coordinates and level taken from room
            with Transaction(self.doc, "Batch create spaces") as tr:
                tr.Start()

                options = tr.GetFailureHandlingOptions()
                failureHandler = ErrorSwallower()
                options.SetFailuresPreprocessor(failureHandler)
                options.SetClearAfterRollback(True)
                tr.SetFailureHandlingOptions(options)
                room_coords = self.transform.OfPoint(room.Location.Point)
                space = self.doc.Create.NewSpace(
                    lvl, UV(room_coords.X, room_coords.Y))

                # Get "REFERENCED_ROOM_UNIQUE_ID" parameter
                ref_id_par = space.GetParameters(self._search_id)[0]
                # Assign room UniqueID to "REFERENCED_ROOM_UNIQUE_ID" parameter
                if ref_id_par:
                    if ref_id_par.StorageType == StorageType.String:
                        ref_id_par.Set(room.UniqueId)
                        self.New_MEPSpaces[
                            room.UniqueId] = self.New_MEPSpaces.get(
                                room.UniqueId, space)
                        self.para_setter(room, space)
                    else:
                        ref_id_par.Set(room.Id.IntegerValue)
                        self.New_MEPSpaces[
                            room.Id.IntegerValue] = self.New_MEPSpaces.get(
                                room.Id.IntegerValue, space)
                        self.para_setter(room, space)
                tr.Commit()

        except Exception as e:
            logger.error(e, exc_info=True)
            pass

        try:
            with Transaction(self.doc, "Set space Id") as tr:
                tr.Start()
                # Get "ID_revit" parameter of a MEP space
                space_id = space.GetParameters("ID_revit")[0]
                # Assign space ID to "ID_revit" parameter
                if space_id:
                    space_id.Set(space.Id.IntegerValue)
                tr.Commit()

        except Exception as e:
            logger.error(e, exc_info=True)
            pass

    def space_updater(self, room, exst_space):
        ''' 
        Function updates existing spaces and moves them if necessary
        '''

        try:
            # Extract space coordinates
            exst_space_coords = exst_space.Location.Point
            # Get room coordinates
            room_coords = self.transform.OfPoint(room.Location.Point)
            # Compare two sets of coordinates
            # If they are almost the same
            if exst_space_coords.IsAlmostEqualTo(room_coords):
                with Transaction(self.doc, "Update parameters") as tr:
                    tr.Start()
                    options = tr.GetFailureHandlingOptions()
                    failureHandler = ErrorSwallower()
                    options.SetFailuresPreprocessor(failureHandler)
                    options.SetClearAfterRollback(True)
                    tr.SetFailureHandlingOptions(options)
                    # Transfer room parameters to a corresponding space
                    self.para_setter(room, exst_space)
                    tr.Commit()
            # Otherwise, move the existing space according to room coordinates
            else:
                move_vector = room_coords - exst_space_coords
                with Transaction(self.doc, "Move spaces") as tr:
                    tr.Start()
                    options = tr.GetFailureHandlingOptions()
                    failureHandler = ErrorSwallower()
                    options.SetFailuresPreprocessor(failureHandler)
                    options.SetClearAfterRollback(True)
                    tr.SetFailureHandlingOptions(options)
                    exst_space.Location.Move(move_vector)
                    # Transfer room parameters to a corresponding space
                    self.para_setter(room, exst_space)
                    tr.Commit()
        except System.MissingMemberException as e:
            logger.error(e, exc_info=True)
            pass

    def space_check(self, Room_UniqueIds):
        ''' 
        Function checks for not placed spaces, obsolete spaces and deletes them
        '''
        # Collect all existing MEP spaces in this document
        for space in self._space_collector.ToElements():

            # Get "REFERENCED_ROOM_UNIQUE_ID" parameter of each space
            id_par_list = space.GetParameters(self._search_id)
            ref_id_par_val = id_par_list[0].AsString()

            # Check if REFERENCED_ROOM_UNIQUE_ID is in room Room_UniqueIds
            # If it's not the case delete the corresponding space
            if space.Area == 0 or ref_id_par_val not in Room_UniqueIds:
                with Transaction(self.doc, "Delete spaces") as tr:
                    tr.Start()

                    try:
                        self.doc.Delete(space.Id)
                    except Exception as e:
                        logger.error(e, exc_info=True)
                        pass

                    tr.Commit()

            else:
                # Otherwise cast it into Existing MEP spaces dictionary
                self.Exist_MEPSpaces[
                    ref_id_par_val] = self.Exist_MEPSpaces.get(
                        ref_id_par_val, space)

            self.counter += 1
            self.ReportProgress.emit(self.counter)

    def para_setter(self, room, space):
        ''' 
        Function transers parameters from the room to newly created spaces
        room: Revit Room, space: MEPSpace
        '''
        # For each parameter in room parameters define if it's shared or builtin parameter
        for par in room.Parameters:
            par_name = par.Definition.Name
            if par.IsShared and par_name in self._excel_parameters:
                # If room parameter is shared get space from Spaces dictionary by UniqueId and extract corresponding space parameter from it by room parameter GUID
                # Depending on the room parameter storage type set its value to the space parameter
                if not par.IsReadOnly:
                    try:
                        space_par = space.get_Parameter(par.GUID)
                        if par.StorageType == StorageType.String and par.HasValue:
                            space_par.Set(par.AsString())
                        elif par.StorageType == StorageType.Integer and par.HasValue:
                            space_par.Set(par.AsInteger())
                        elif par.StorageType == StorageType.Double and par.HasValue:
                            space_par.Set(par.AsDouble())
                    except Exception as e:
                        logger.error(e, exc_info=True)
                        pass

                    self.counter += 1
                    self.ReportProgress.emit(self.counter)

            elif par.Definition.BuiltInParameter != BuiltInParameter.INVALID\
                and LabelUtils.GetLabelFor(par.Definition.BuiltInParameter) in self._excel_parameters:
                # If room parameter is builtin get space from Spaces dictionary by UniqueId and extract corresponding space parameter from it by builtin parameter
                # Depending on the room parameter storage type set its value to the space parameter
                if not par.IsReadOnly:
                    try:
                        space_par = space.get_Parameter(par.Definition)
                        if par.StorageType == StorageType.String and par.HasValue:
                            space_par.Set(par.AsString())
                        elif par.StorageType == StorageType.Integer and par.HasValue:
                            space_par.Set(par.AsInteger())
                        elif par.StorageType == StorageType.Double and par.HasValue:
                            space_par.Set(par.AsDouble())

                    except Exception as e:
                        logger.error(e, exc_info=True)
                        pass

                    self.counter += 1
                    self.ReportProgress.emit(self.counter)

    def delete_spaces(self):
        ''' 
        Function deletes spaces
        '''
        self.counter = 0

        space_collector = FilteredElementCollector(
            self.doc).WhereElementIsNotElementType().OfCategory(
                BuiltInCategory.OST_MEPSpaces)
        spaces = space_collector.ToElements()
        self.startProgress.emit(len(spaces))

        with Transaction(self.doc, "Delete spaces") as tr:
            tr.Start()

            for space in spaces:

                try:
                    self.doc.Delete(space.Id)
                    self.counter += 1
                    self.ReportProgress.emit(self.counter)

                except Exception as e:
                    logger.error(e, exc_info=True)
                    pass

            tr.Commit()

            self.New_MEPSpaces = {}
            self.Exist_MEPSpaces = {}
            self.endProgress.emit()

    def merge_two_dicts(self, d1, d2):
        '''
        Merges two dictionaries
        Returns a merged dictionary
        '''
        d_merged = d1.copy()
        d_merged.update(d2)
        return d_merged

    def write_to_excel(self):
        """
        Writing parameters to an Excel workbook
        """
        self.counter = 0
        self.startProgress.emit(self._space_collector.GetElementCount() + \
                ((self._space_collector.GetElementCount()*len(self._excel_parameters))))

        # Obtaining existing spaces
        self.__get_exist_spaces()
        # Write data to an excel file
        self.__write_to_excel(self._excel_parameters)

    def __get_exist_spaces(self):
        """
        Obtains existing spaces
        Fills the Exist_MEPSpaces dictionary: key: reference_room_id, value: MEP space
        """
        self.Exist_MEPSpaces = {}
        # Collect all existing MEP spaces in this document
        for space in self._space_collector:

            try:
                # Get "REFERENCED_ROOM_UNIQUE_ID" parameter of each space
                id_par_list = space.GetParameters(self._search_id)
                ref_id_par_val = id_par_list[0].AsString()

                # Cast space into Existing MEP spaces dictionary
                self.Exist_MEPSpaces[
                    ref_id_par_val] = self.Exist_MEPSpaces.get(
                        ref_id_par_val, space)

                self.counter += 1
                self.ReportProgress.emit(self.counter)

            except Exception as e:
                logger.error(e, exc_info=True)
                pass

    def __write_to_excel(self, params_to_write):
        """
        Writing parameters to an Excel workbook
        (private method)
        """
        units = {
            UnitType.UT_Length: DisplayUnitType.DUT_METERS,
            UnitType.UT_Area: DisplayUnitType.DUT_SQUARE_METERS,
            UnitType.UT_Volume: DisplayUnitType.DUT_CUBIC_METERS,
            UnitType.UT_Number: DisplayUnitType.DUT_PERCENTAGE,
            UnitType.UT_HVAC_Heating_Load: DisplayUnitType.DUT_KILOWATTS,
            UnitType.UT_HVAC_Cooling_Load: DisplayUnitType.DUT_KILOWATTS,
            UnitType.UT_HVAC_Airflow_Density:
            DisplayUnitType.DUT_CUBIC_FEET_PER_MINUTE_SQUARE_FOOT,
            UnitType.UT_HVAC_Airflow: DisplayUnitType.DUT_CUBIC_METERS_PER_HOUR
        }

        columns = list(string.ascii_uppercase) + [
            "".join(i) for i in product(string.ascii_uppercase, repeat=2)
        ]
        params = dict(izip(params_to_write, columns))

        try:
            workBook = self.excel.Workbooks.Open(r'{}'.format(
                self._xl_file_path))
            workSheet = workBook.Worksheets(1)

        except Exception as e:
            logger.error(e, exc_info=True)
            pass

        for par, col in params.items():
            workSheet.Cells[1, col] = par
        row = 2

        MEPSpaces = self.merge_two_dicts(self.New_MEPSpaces,
                                         self.Exist_MEPSpaces)

        for space_id, space in MEPSpaces.items():
            try:
                workSheet.Cells[row, "A"] = space_id
                for par in space.Parameters:
                    par_name = par.Definition.Name
                    if par_name in params.keys():
                        if par.StorageType == StorageType.String:
                            workSheet.Cells[row,
                                            params[par_name]] = par.AsString()
                        elif par.StorageType == StorageType.Integer:
                            workSheet.Cells[
                                row, params[par_name]] = par.AsInteger()
                        else:
                            conv_val = UnitUtils.ConvertFromInternalUnits(
                                par.AsDouble(),
                                units.get(par.Definition.UnitType,
                                          DisplayUnitType.DUT_GENERAL))
                            workSheet.Cells[row, params[par_name]] = conv_val
                        self.counter += 1
                        self.ReportProgress.emit(self.counter)
                row += 1

            except Exception as e:
                logger.error(e, exc_info=True)
                pass

        # makes the Excel application visible to the user
        self.excel.Visible = True