Skip to content

tIED: removeIeds behaviours with LNodes #121

Closed

Description

@danyill

Currently I find that the behaviour of removeIED for LNodes is to replace the IED name with None when the IED is removed.

This behaviour is problematic for me when removing a number of IEDs in the Remove IEDs plugin in Transpower's distribution.

It leaves a large number of dangling LNode elements.

For various reasons we often map an LPHD node for each IED against a busbar bay.

After removing several IEDs this section looks like this:

	<Function name="BusPhysConnection">
		<LNode iedName="None" lnClass="LPHD" lnInst="0"/>
		<LNode iedName="XAT_242_C1" ldInst="CFG" lnClass="LPHD" lnInst="0"/>
		<LNode iedName="None" lnClass="LPHD" lnInst="0"/>
		<LNode iedName="None" prefix="DevID" lnClass="LPHD" lnInst="1"/>
		<LNode iedName="None" prefix="DevID" lnClass="LPHD" lnInst="1"/>
		<LNode iedName="None" lnClass="LPHD" lnInst="1"/>
		<LNode iedName="None" prefix="DevID" lnClass="LPHD" lnInst="1"/>
		<LNode iedName="None" prefix="DevID" lnClass="LPHD" lnInst="1"/>
		<LNode iedName="None" lnClass="LPHD" lnInst="1"/>
		<LNode iedName="None" lnClass="LPHD" lnInst="1"/>
		<LNode iedName="None" lnClass="LPHD" lnInst="1"/>
		<LNode iedName="None" lnClass="LPHD" lnInst="1"/>
		<LNode iedName="None" lnClass="LPHD" lnInst="0"/>
		<LNode iedName="None" lnClass="LPHD" lnInst="0"/>
		<LNode iedName="None" lnClass="LPHD" lnInst="0"/>
		<LNode iedName="None" lnClass="LPHD" lnInst="0"/>
		<LNode iedName="None" lnClass="LPHD" lnInst="0"/>
		<LNode iedName="None" lnClass="LPHD" lnInst="0"/>
	</Function>

This however becomes a schema violation:

image

The reason for this is spelt out in Table 8 of IEC 61850 Ed 2.1 and the subsequent text:

For specifications where iedName=”None” everywhere, the combination of the other attributes
must be unique within the same level. This means e.g. that the prefix or lnInst should be
different if several LNs with identical lnClass are used within the same substation part (i.e.
same bay). This should also be the case for IED names, if application related naming (see
8.5.4) shall be used. This is NOT checked by the SCL schema, therefore it is the
responsibility of the project engineer or system tool, if application related naming shall be
used additionally to product (IED)-related naming.

What should the behaviour of the removeIED function be here?

In my view at the least we need to provide an option to allow removal of LNodes to allow a bulk delete of IEDs without dangling, unnecessary schema non-compliance which is at least one common workflow option. I am confident it is also a valid workflow to sometimes retain the LNodes so I think it is a good case for an optional argument.

@JakobVogelsang WDYT?

Would you accept:

export type RemoveIedOptions = {
  removeLNodes: boolean;
};

function removeIED(remove: Remove, 
                   options: RemoveIedOptions = { removeLNodes: false }): (Update | Remove)[] {
 ... 
}

If you are happy with this call signature I can prepare a PR.

Activity

  1. JakobVogelsang commented on Dec 5, 2024

    @JakobVogelsang
    Contributor

    Hi Dan, I would propose to add an option to removeLNode.

  2. JakobVogelsang commented on Dec 5, 2024

    @JakobVogelsang
    Contributor

    Would you mind it being a default false?

  3. danyill commented on Dec 5, 2024

    @danyill
    CollaboratorAuthor
  4. danyill commented on Jan 9, 2025

    @danyill
    CollaboratorAuthor

    I think having a default of false is appropriate for an updateIED function (for a single IED). Although it could pose a problem there if there was already a iedName of None and matching other attributes.

    If we had an updateIEDs function I'd use true because otherwise we would likely break schema validity (or need to check and remove as necessary) if we had multiple LNodes with matching attributes across multiple IEDs.

    That posits a little challenge because the user should not see different behaviour when removing one IED vs. more than one IED. So I'd like to propose a default of true.

  5. JakobVogelsang commented on Jun 5, 2025

    @JakobVogelsang
    Contributor

    This is very reasonable. Let's got with true then :)

  6. ca-d commented on Apr 27, 2026

    @ca-d
    Contributor

    I would have guessed that someone removing an IED would want one of these two:

    1. Remove all LNodes pointing into this IED entirely without any substitute
    2. Virtualize all LNodes pointing into this IED by setting iedName=“None”, in which case duplicate LNodes in the same place are always to be avoided.
      In particular, I cannot see a need for ever generating duplicate LNodes and thereby invalidating the file.

    @JakobVogelsang @danyill Am I getting the situation wrong? If so, in what way?

  7. JakobVogelsang commented on Apr 29, 2026

    @JakobVogelsang
    Contributor

    Hi you all, Stephen has triggered me today with this issue. I have another proposal for discussion @danyill @ca-d:

    If an LNode element has no lnType then we remove the LNode element.
    If an LNode element HAS a lnType then we do not remove but set the iedName to None.

  8. danyill commented on May 1, 2026

    @danyill
    CollaboratorAuthor

    Hi you all, Stephen has triggered me today with this issue. I have another proposal for discussion @danyill @ca-d:

    If an LNode element has no lnType then we remove the LNode element. If an LNode element HAS a lnType then we do not remove but set the iedName to None.

    That seems incomplete to me... I think though it's heading in the right direction and the suggestion from @ca-d has merit.

    The uniqueness constraint in IEC 61850-6 is something like (for a LNode within a Function):

    <xs:element name="Function" type="tFunction" minOccurs="0" maxOccurs="unbounded">
    	<xs:unique name="uniqueLNodeInFunctionSS">
    		<xs:selector xpath="./scl:LNode"/>
    		<xs:field xpath="@lnInst"/>
    		<xs:field xpath="@lnClass"/>
    		<xs:field xpath="@iedName"/>
    		<xs:field xpath="@ldInst"/>
    		<xs:field xpath="@prefix"/>
    	</xs:unique>
    	<xs:unique name="uniqueChildNameInSubstationFunc">
    		<xs:selector xpath="./*"/>
    		<xs:field xpath="@name"/>
    	</xs:unique>
    </xs:element>

    Given we require uniqueness across multiple fields perhaps what we should implement is:

    If removing the iedName is sufficient to guarantee uniqueness within the appropriate location, then just do that, otherwise check all other uniqueness criteria and if they would be broken then remove the LNode.

    The standard requires:

    • For iedName != "None": (iedName, ldInst, prefix, lnClass, lnInst) unique across all Substation sections.
    • For iedName = "None": (ldInst, prefix, lnClass, lnInst) unique within the same substation container (Bay, VoltageLevel, or Substation).

    Perhaps something like this?

    import { type EditV2 } from '@openscd/oscd-api';
    
    /**
     * Build the edits required to detach all public LNode bindings to `iedName`
     * from the substation model, preserving each as a specification entry with 
     * (iedName="None") where possible.
     */
    export function detachLNodeBindings(
    	doc: XMLDocument,
    	iedName: string,
    ): EditV2[] {
    	const KEY_ATTRS = ['lnClass', 'lnInst', 'ldInst', 'prefix'] as const;
    	
    	const CONTAINER= 'Bay, VoltageLevel, Substation';
    
    	const lnodeKey = (ln: Element): string =>
    		KEY_ATTRS.map(a => ln.getAttribute(a) ?? '').join('|');
    
    	const isPublicSubstationLNode = (ln: Element): boolean =>
    		!ln.closest('Private') && ln.closest('Substation') !== null;
    
    	const candidates = Array.from(doc.querySelectorAll('LNode'))
    		.filter(ln =>
    			ln.getAttribute('iedName') === iedName &&
    			isPublicSubstationLNode(ln),
    		);
    
    	if (candidates.length === 0) return [];
    
    	const candidateSet = new Set(candidates);
    
    	const claimed = new Map<Element, Set<string>>();
    	for (const ln of Array.from(doc.querySelectorAll('LNode[iedName="None"]'))) {
    		if (candidateSet.has(ln)) continue;
    		if (!isPublicSubstationLNode(ln)) continue;
    		const cont= ln.closest(CONTAINER);
    		if (!cont) continue;
    		let keys = claimed.get(cont);
    		if (!keys) claimed.set(cont, (keys = new Set()));
    		keys.add(lnodeKey(ln));
    	}
    
    	const edits: EditV2[] = [];
    
    	for (const ln of candidates) {
    		const cont= ln.closest(CONTAINER);
    		if (!cont) {
    			edits.push({ node: ln });
    			continue;
    		}
    		const key = lnodeKey(ln);
    		let keys = claimed.get(cont);
    		if (!keys) claimed.set(cont, (keys = new Set()));
    
    		if (keys.has(key)) {
    			edits.push({ node: ln });
    		} else {
    			keys.add(key);
    			if (ln.getAttribute('iedName') !== 'None') {
    				edits.push({ element: ln, attributes: { iedName: 'None' } });
    			}
    		}
    	}
    
    	return edits;
    }

    This would be for a use case of:

    • You remove an instantiated IED but you wish its functional linkages to remain so you can instantiate another IED against them.
    • In this case something like the above would allow removing and re-instantiating a single IED but not probably more than one at the same time because of risk of conflicts.

    Even if we did this (as a default) I'd probably suggest that it was a valid use case to also say "I don't want this IED or the functions it represents so let's remove the whole lot" so it would be perhaps an additional flag. You either delete the LNodes or you retain them if possible ("virtualise the IED") so we should implement as an option.

  9. added a commit that references this issue on May 27, 2026
    4ef2497
  10. stee-re commented on May 27, 2026

    @stee-re
    Collaborator

    The fix for this was revised, based on the discussions here. I waas forced to create a new PR #147 for this (because of the repo transfer).
    The default behaviour now is LNodes bound to the IED being removed, are also removed. But a new optional flag "preserveLNodes" has been exposed, which will update the LNodes iedName to None if doing so won't cause a duplicate. If it would cause a duplicate, it is simply removed instead.

  11. added a commit that references this issue on Jul 12, 2026
    31c619f
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions