package io.openems.edge.evcs.api;

import static io.openems.edge.common.type.TypeUtils.divide;

import java.util.function.Consumer;

import io.openems.common.channel.AccessMode;
import io.openems.common.channel.Level;
import io.openems.common.channel.PersistencePriority;
import io.openems.common.channel.Unit;
import io.openems.common.types.MeterType;
import io.openems.common.types.OpenemsType;
import io.openems.edge.common.channel.Channel;
import io.openems.edge.common.channel.Doc;
import io.openems.edge.common.channel.EnumReadChannel;
import io.openems.edge.common.channel.IntegerReadChannel;
import io.openems.edge.common.channel.StateChannel;
import io.openems.edge.common.channel.value.Value;
import io.openems.edge.common.component.OpenemsComponent;
import io.openems.edge.common.modbusslave.ModbusSlaveNatureTable;
import io.openems.edge.common.modbusslave.ModbusType;
import io.openems.edge.meter.api.ElectricityMeter;

/**
 * Represents an Electric Vehicle Charging Station.
 * 
 * <p>
 * Not all EVCS provide all necessary values for an {@link ElectricityMeter}.
 * Therefore consider using one of the helper methods which are useful for EVCS
 * only:
 * <ul>
 * <li>{@link #addCalculatePowerLimitListeners(Evcs)}
 * <li>{@link #calculateUsedPhasesFromCurrent(Evcs)}
 * <li>{@link #calculatePhasesFromActivePowerAndPhaseCurrents(Evcs)}
 * <li>{@link #evaluatePhaseCountFromCurrent(Integer, Integer, Integer)}
 * </ul>
 */
public interface Evcs extends ElectricityMeter, OpenemsComponent {

	public static final Integer DEFAULT_MAXIMUM_HARDWARE_POWER = 22_080; // W
	public static final Integer DEFAULT_MINIMUM_HARDWARE_POWER = 4_140; // W
	public static final Integer DEFAULT_MAXIMUM_HARDWARE_CURRENT = 32_000; // mA
	public static final Integer DEFAULT_MINIMUM_HARDWARE_CURRENT = 6_000; // mA
	public static final Integer DEFAULT_VOLTAGE = 230; // V
	public static final int DEFAULT_POWER_RECISION = 230;
	public static final int MIN_EVCS_ACTIVITY_CURRENT = 450; // mA (~100W)

	public enum ChannelId implements io.openems.edge.common.channel.ChannelId {

		/**
		 * Status.
		 *
		 * <p>
		 * The Status of the EVCS charging station.
		 *
		 * <ul>
		 * <li>Interface: Evcs
		 * <li>Readable
		 * <li>Type: Status
		 * </ul>
		 */
		STATUS(Doc.of(Status.values()) //
				.persistencePriority(PersistencePriority.HIGH)), //

		/**
		 * Charging Type.
		 *
		 * <p>
		 * Type of charging.
		 *
		 * <ul>
		 * <li>Interface: Evcs
		 * <li>Readable
		 * <li>Type: ChargingType
		 * </ul>
		 */
		CHARGING_TYPE(Doc.of(ChargingType.values()) //
				.persistencePriority(PersistencePriority.HIGH)), //

		/**
		 * Count of phases, the EV is charging with.
		 *
		 * <p>
		 * This value is derived from the charging station or calculated during the
		 * charging. When this value is set, the minimum and maximum limits are set at
		 * the same time if the EVCS is a {@link ManagedEvcs}.
		 *
		 * <ul>
		 * <li>Interface: Evcs
		 * <li>Readable
		 * <li>Type: Integer
		 * </ul>
		 */
		PHASES(Doc.of(Phases.values()) //
				.debounce(5) //
				.persistencePriority(PersistencePriority.HIGH)), //

		/**
		 * Fixed minimum power allowed by the hardware in W.
		 *
		 * <p>
		 * Maximum of the configured minimum hardware limit and the read or given
		 * minimum hardware limit - e.g. KEBA minimum requirement is 6A = 4140W and the
		 * component configuration is 10A = 6900W because the customer wants to ensure
		 * that his Renault ZOE is always charging with an acceptable efficiency. In
		 * this case the Channel should be set to 6900W. Used power instead of current,
		 * because it is easier to store it in an Integer Channel. When this value is
		 * set, the minimum and maximum limits are set at the same time if the EVCS is a
		 * {@link ManagedEvcs}.
		 *
		 * <ul>
		 * <li>Interface: Evcs
		 * <li>Readable
		 * <li>Type: Integer
		 * <li>Unit: W
		 * </ul>
		 */
		FIXED_MINIMUM_HARDWARE_POWER(Doc.of(OpenemsType.INTEGER) //
				.unit(Unit.WATT) //
				.persistencePriority(PersistencePriority.HIGH)), //

		/**
		 * Fixed maximum power allowed by the hardware in W.
		 *
		 * <p>
		 * Minimum of the configured maximum hardware limit and the read maximum
		 * hardware limit - e.g. KEBA Dip-Switch Settings set to 32A = 22080W and
		 * component configuration of 16A = 11040. In this case the Channel should be
		 * set to 11040W. Used power instead of current, because it is easier to store
		 * it in an Integer Channel. When this value is set, the minimum and maximum
		 * limits are a Integer Channel. When this value is set, the minimum and maximum
		 * limits are set at the same time if the EVCS is a {@link ManagedEvcs}.
		 *
		 * <ul>
		 * <li>Interface: Evcs
		 * <li>Readable
		 * <li>Type: Integer
		 * <li>Unit: W
		 * </ul>
		 */
		FIXED_MAXIMUM_HARDWARE_POWER(Doc.of(OpenemsType.INTEGER) //
				.unit(Unit.WATT) //
				.persistencePriority(PersistencePriority.HIGH)), //

		/**
		 * Minimum hardware power in W calculated with the current used Phases, used for
		 * the boundaries of the monitoring.
		 *
		 * <p>
		 * This minimum limit is dynamically set depending on the current used
		 * {@link #PHASES} and the {@link #FIXED_MINIMUM_HARDWARE_POWER}, to be able to
		 * react on different power limits if some of the Phases are not used - e.g. The
		 * minimum and maximum of a charger is 6 and 32 Ampere. Because the default unit
		 * of all OpenEMS calculations is power, the real minimum for charging on one
		 * Phase is not 4140 Watt but 1380 Watt (Or in current 6A|0A|0A).
		 *
		 * <ul>
		 * <li>Interface: Evcs
		 * <li>Readable
		 * <li>Type: Integer
		 * <li>Unit: W
		 * </ul>
		 */
		MINIMUM_HARDWARE_POWER(Doc.of(OpenemsType.INTEGER) //
				.unit(Unit.WATT) //
				.persistencePriority(PersistencePriority.HIGH)), //

		/**
		 * Maximum hardware power in W calculated with the current used Phases, used for
		 * the boundaries of the monitoring.
		 *
		 * <p>
		 * This maximum limit is dynamically set depending on the current used
		 * {@link #PHASES} and the {@link #FIXED_MINIMUM_HARDWARE_POWER}, to be able to
		 * react on different power limits if some of the Phases are not used - e.g. The
		 * minimum and maximum of a charger is 6 and 32 Ampere. Because the default unit
		 * of all OpenEMS calculations is power, the real maximum for charging on one
		 * Phase is not 22080 Watt but 7360 Watt (Or in current 32A|0A|0A).
		 *
		 * <ul>
		 * <li>Interface: Evcs
		 * <li>Readable
		 * <li>Type: Integer
		 * <li>Unit: W
		 * </ul>
		 */
		MAXIMUM_HARDWARE_POWER(Doc.of(OpenemsType.INTEGER) //
				.unit(Unit.WATT) //
				.persistencePriority(PersistencePriority.HIGH)), //

		/**
		 * Maximum Power defined by software.
		 *
		 * <ul>
		 * <li>Interface: Evcs
		 * <li>Readable
		 * <li>Type: Integer
		 * <li>Unit: W
		 * </ul>
		 */
		MAXIMUM_POWER(Doc.of(OpenemsType.INTEGER) //
				.unit(Unit.WATT) //
				.persistencePriority(PersistencePriority.HIGH)), //

		/**
		 * Minimum Power defined by software.
		 *
		 * <ul>
		 * <li>Interface: Evcs
		 * <li>Readable
		 * <li>Type: Integer
		 * <li>Unit: W
		 * </ul>
		 */
		MINIMUM_POWER(Doc.of(OpenemsType.INTEGER) //
				.unit(Unit.WATT) //
				.persistencePriority(PersistencePriority.HIGH)), //

		/**
		 * Energy that was charged during the current or last Session.
		 *
		 * <ul>
		 * <li>Interface: Evcs
		 * <li>Readable
		 * <li>Type: Integer
		 * <li>Unit: Wh
		 * </ul>
		 */
		ENERGY_SESSION(Doc.of(OpenemsType.INTEGER) //
				.unit(Unit.WATT_HOURS) //
				.persistencePriority(PersistencePriority.HIGH)), //

		/**
		 * Failed state channel for a failed communication to the EVCS.
		 *
		 * <ul>
		 * <li>Interface: Evcs
		 * <li>Readable
		 * <li>Level: FAULT
		 * </ul>
		 */
		CHARGINGSTATION_COMMUNICATION_FAILED(Doc.of(Level.FAULT) //
				.persistencePriority(PersistencePriority.HIGH) //
				.text("Chargingstation Communication Failed " //
						+ "| Keine Verbindung zur Ladestation " //
						+ "| Bitte überprüfen Sie die Kommunikationsverbindung zu der Ladestation")); //

		private final Doc doc;

		private ChannelId(Doc doc) {
			this.doc = doc;
		}

		@Override
		public Doc doc() {
			return this.doc;
		}
	}

	/**
	 * Gets the Channel for {@link ChannelId#STATUS}.
	 *
	 * @return the Channel
	 */
	public default Channel<Status> getStatusChannel() {
		return this.channel(ChannelId.STATUS);
	}

	/**
	 * Gets the Status of the EVCS charging station. See {@link ChannelId#STATUS}.
	 *
	 * @return the Channel {@link Value}
	 */
	public default Status getStatus() {
		return this.getStatusChannel().value().asEnum();
	}

	/**
	 * Internal method to set the 'nextValue' on {@link ChannelId#STATUS} Channel.
	 *
	 * @param value the next value
	 */
	public default void _setStatus(Status value) {
		this.getStatusChannel().setNextValue(value);
	}

	/**
	 * Gets the Channel for {@link ChannelId#CHARGING_TYPE}.
	 *
	 * @return the Channel
	 */
	public default Channel<ChargingType> getChargingTypeChannel() {
		return this.channel(ChannelId.CHARGING_TYPE);
	}

	/**
	 * Gets the Type of charging. See {@link ChannelId#CHARGING_TYPE}.
	 *
	 * @return the Channel {@link Value}
	 */
	public default ChargingType getChargingType() {
		return this.getChargingTypeChannel().value().asEnum();
	}

	/**
	 * Internal method to set the 'nextValue' on {@link ChannelId#CHARGING_TYPE}
	 * Channel.
	 *
	 * @param value the next value
	 */
	public default void _setChargingType(ChargingType value) {
		this.getChargingTypeChannel().setNextValue(value);
	}

	/**
	 * Gets the Channel for {@link ChannelId#PHASES}.
	 *
	 * @return the Channel
	 */
	public default EnumReadChannel getPhasesChannel() {
		return this.channel(ChannelId.PHASES);
	}

	/**
	 * Gets the current Phases definition. See {@link ChannelId#PHASES}.
	 *
	 * @return the Channel {@link Value}
	 */
	public default Phases getPhases() {
		return this.getPhasesChannel().value().asEnum();
	}

	/**
	 * Gets the Count of phases, the EV is charging with. See
	 * {@link ChannelId#PHASES}.
	 *
	 * @return the Channel {@link Value}
	 */
	public default int getPhasesAsInt() {
		return this.getPhasesChannel().value().asEnum().getValue();
	}

	/**
	 * Internal method to set the 'nextValue' on {@link ChannelId#PHASES} Channel.
	 *
	 * @param value the next value
	 */
	public default void _setPhases(Phases value) {
		this.getPhasesChannel().setNextValue(value);
	}

	/**
	 * Internal method to set the 'nextValue' on {@link ChannelId#PHASES} Channel.
	 *
	 * @param value the next value
	 */
	public default void _setPhases(Integer value) {
		if (value == null || value == 0) {
			this._setPhases(Phases.THREE_PHASE);
			return;
		}
		switch (value) {
		case 1:
			this._setPhases(Phases.ONE_PHASE);
			break;
		case 2:
			this._setPhases(Phases.TWO_PHASE);
			break;
		case 3:
			this._setPhases(Phases.THREE_PHASE);
			break;
		default:
			throw new IllegalArgumentException("Value [" + value + "] for _setPhases is invalid");
		}
	}

	/**
	 * Gets the Channel for {@link ChannelId#FIXED_MINIMUM_HARDWARE_POWER}.
	 *
	 * @return the Channel
	 */
	public default IntegerReadChannel getFixedMinimumHardwarePowerChannel() {
		return this.channel(ChannelId.FIXED_MINIMUM_HARDWARE_POWER);
	}

	/**
	 * Gets the fixed minimum power valid by the hardware in [W]. See
	 * {@link ChannelId#FIXED_MINIMUM_HARDWARE_POWER}.
	 *
	 * @return the Channel {@link Value}
	 */
	public default Value<Integer> getFixedMinimumHardwarePower() {
		return this.getFixedMinimumHardwarePowerChannel().value();
	}

	/**
	 * Internal method to set the 'nextValue' on
	 * {@link ChannelId#FIXED_MINIMUM_HARDWARE_POWER} Channel.
	 *
	 * @param value the next value
	 */
	public default void _setFixedMinimumHardwarePower(Integer value) {
		this.getFixedMinimumHardwarePowerChannel().setNextValue(value);
	}

	/**
	 * Internal method to set the 'nextValue' on
	 * {@link ChannelId#FIXED_MINIMUM_HARDWARE_POWER} Channel.
	 *
	 * @param value the next value
	 */
	public default void _setFixedMinimumHardwarePower(int value) {
		this.getFixedMinimumHardwarePowerChannel().setNextValue(value);
	}

	/**
	 * Gets the Channel for {@link ChannelId#FIXED_MAXIMUM_HARDWARE_POWER}.
	 *
	 * @return the Channel
	 */
	public default IntegerReadChannel getFixedMaximumHardwarePowerChannel() {
		return this.channel(ChannelId.FIXED_MAXIMUM_HARDWARE_POWER);
	}

	/**
	 * Gets the fixed maximum power valid by the hardware in [W]. See
	 * {@link ChannelId#FIXED_MAXIMUM_HARDWARE_POWER}.
	 *
	 * @return the Channel {@link Value}
	 */
	public default Value<Integer> getFixedMaximumHardwarePower() {
		return this.getFixedMaximumHardwarePowerChannel().value();
	}

	/**
	 * Internal method to set the 'nextValue' on
	 * {@link ChannelId#FIXED_MAXIMUM_HARDWARE_POWER} Channel.
	 *
	 * @param value the next value
	 */
	public default void _setFixedMaximumHardwarePower(Integer value) {
		this.getFixedMaximumHardwarePowerChannel().setNextValue(value);
	}

	/**
	 * Internal method to set the 'nextValue' on
	 * {@link ChannelId#FIXED_MAXIMUM_HARDWARE_POWER} Channel.
	 *
	 * @param value the next value
	 */
	public default void _setFixedMaximumHardwarePower(int value) {
		this.getFixedMaximumHardwarePowerChannel().setNextValue(value);
	}

	/**
	 * Gets the Channel for {@link ChannelId#MAXIMUM_HARDWARE_POWER}.
	 *
	 * @return the Channel
	 */
	public default IntegerReadChannel getMaximumHardwarePowerChannel() {
		return this.channel(ChannelId.MAXIMUM_HARDWARE_POWER);
	}

	/**
	 * Gets the Maximum Power valid by the hardware in [W]. See
	 * {@link ChannelId#MAXIMUM_HARDWARE_POWER}.
	 *
	 * @return the Channel {@link Value}
	 */
	public default Value<Integer> getMaximumHardwarePower() {
		return this.getMaximumHardwarePowerChannel().value();
	}

	/**
	 * Gets the Channel for {@link ChannelId#MINIMUM_HARDWARE_POWER}.
	 *
	 * @return the Channel
	 */
	public default IntegerReadChannel getMinimumHardwarePowerChannel() {
		return this.channel(ChannelId.MINIMUM_HARDWARE_POWER);
	}

	/**
	 * Gets the Minimum Power valid by the hardware in [W]. See
	 * {@link ChannelId#MINIMUM_HARDWARE_POWER}.
	 *
	 * @return the Channel {@link Value}
	 */
	public default Value<Integer> getMinimumHardwarePower() {
		return this.getMinimumHardwarePowerChannel().value();
	}

	/**
	 * Gets the Channel for {@link ChannelId#MAXIMUM_POWER}.
	 *
	 * @return the Channel
	 */
	public default IntegerReadChannel getMaximumPowerChannel() {
		return this.channel(ChannelId.MAXIMUM_POWER);
	}

	/**
	 * Gets the Maximum Power valid by software in [W]. See
	 * {@link ChannelId#MAXIMUM_POWER}.
	 *
	 * @return the Channel {@link Value}
	 */
	public default Value<Integer> getMaximumPower() {
		return this.getMaximumPowerChannel().value();
	}

	/**
	 * Internal method to set the 'nextValue' on {@link ChannelId#MAXIMUM_POWER}
	 * Channel.
	 *
	 * @param value the next value
	 */
	public default void _setMaximumPower(Integer value) {
		this.getMaximumPowerChannel().setNextValue(value);
	}

	/**
	 * Internal method to set the 'nextValue' on {@link ChannelId#MAXIMUM_POWER}
	 * Channel.
	 *
	 * @param value the next value
	 */
	public default void _setMaximumPower(int value) {
		this.getMaximumPowerChannel().setNextValue(value);
	}

	/**
	 * Gets the Channel for {@link ChannelId#MINIMUM_POWER}.
	 *
	 * @return the Channel
	 */
	public default IntegerReadChannel getMinimumPowerChannel() {
		return this.channel(ChannelId.MINIMUM_POWER);
	}

	/**
	 * Gets the Minimum Power valid by software in [W]. See
	 * {@link ChannelId#MINIMUM_POWER}.
	 *
	 * @return the Channel {@link Value}
	 */
	public default Value<Integer> getMinimumPower() {
		return this.getMinimumPowerChannel().value();
	}

	/**
	 * Internal method to set the 'nextValue' on {@link ChannelId#MINIMUM_POWER}
	 * Channel.
	 *
	 * @param value the next value
	 */
	public default void _setMinimumPower(Integer value) {
		this.getMinimumPowerChannel().setNextValue(value);
	}

	/**
	 * Internal method to set the 'nextValue' on {@link ChannelId#MINIMUM_POWER}
	 * Channel.
	 *
	 * @param value the next value
	 */
	public default void _setMinimumPower(int value) {
		this.getMinimumPowerChannel().setNextValue(value);
	}

	/**
	 * Gets the Channel for {@link ChannelId#ENERGY_SESSION}.
	 *
	 * @return the Channel
	 */
	public default IntegerReadChannel getEnergySessionChannel() {
		return this.channel(ChannelId.ENERGY_SESSION);
	}

	/**
	 * Gets the Energy that was charged during the current or last Session in [Wh].
	 * See {@link ChannelId#ENERGY_SESSION}.
	 *
	 * @return the Channel {@link Value}
	 */
	public default Value<Integer> getEnergySession() {
		return this.getEnergySessionChannel().value();
	}

	/**
	 * Internal method to set the 'nextValue' on {@link ChannelId#ENERGY_SESSION}
	 * Channel.
	 *
	 * @param value the next value
	 */
	public default void _setEnergySession(Integer value) {
		this.getEnergySessionChannel().setNextValue(value);
	}

	/**
	 * Internal method to set the 'nextValue' on {@link ChannelId#ENERGY_SESSION}
	 * Channel.
	 *
	 * @param value the next value
	 */
	public default void _setEnergySession(int value) {
		this.getEnergySessionChannel().setNextValue(value);
	}

	/**
	 * Gets the Channel for {@link ChannelId#CHARGINGSTATION_COMMUNICATION_FAILED}.
	 *
	 * @return the Channel
	 */
	public default StateChannel getChargingstationCommunicationFailedChannel() {
		return this.channel(ChannelId.CHARGINGSTATION_COMMUNICATION_FAILED);
	}

	/**
	 * Gets the Failed state channel for a failed communication to the EVCS. See
	 * {@link ChannelId#CHARGINGSTATION_COMMUNICATION_FAILED}.
	 *
	 * @return the Channel {@link Value}
	 */
	public default Value<Boolean> getChargingstationCommunicationFailed() {
		return this.getChargingstationCommunicationFailedChannel().value();
	}

	/**
	 * Internal method to set the 'nextValue' on
	 * {@link ChannelId#CHARGINGSTATION_COMMUNICATION_FAILED} Channel.
	 *
	 * @param value the next value
	 */
	public default void _setChargingstationCommunicationFailed(boolean value) {
		this.getChargingstationCommunicationFailedChannel().setNextValue(value);
	}

	/**
	 * Gets the {@link MeterType} of an {@link Evcs} {@link ElectricityMeter}.
	 * 
	 * @return the {@link MeterType}
	 */
	@Override
	public default MeterType getMeterType() {
		return this.isReadOnly() //
				? MeterType.CONSUMPTION_METERED //
				: MeterType.MANAGED_CONSUMPTION_METERED;
	}

	/**
	 * Adds onSetNextValue listeners for minimum and maximum hardware power.
	 *
	 * <p>
	 * Since the minimum and maximum power strongly depends on the connected
	 * vehicle, this automatically adjusts it to the currently used phases.
	 *
	 * @param evcs evcs
	 */
	// TODO: @clehne 2024.08.20 implementation may be broken on some chargepoint
	// implementations. Reason: not all implementations always update nextValue
	// in every cycle. Therefore nextValue is null from time to time.
	// Suggestion: method should work on value()
	public static void addCalculatePowerLimitListeners(Evcs evcs) {

		final Consumer<Value<Integer>> calculateHardwarePowerLimits = ignore -> {

			Phases phases = evcs.getPhasesChannel().getNextValue().asEnum();
			int fixedMaximum = evcs.getFixedMaximumHardwarePowerChannel().getNextValue()
					.orElse(DEFAULT_MAXIMUM_HARDWARE_POWER);
			int fixedMinimum = evcs.getFixedMinimumHardwarePowerChannel().getNextValue()
					.orElse(DEFAULT_MINIMUM_HARDWARE_POWER);

			var maximumPower = phases.getFromThreePhase(fixedMaximum);
			var minimumPower = phases.getFromThreePhase(fixedMinimum);

			evcs.getMaximumHardwarePowerChannel().setNextValue(maximumPower);
			evcs.getMinimumHardwarePowerChannel().setNextValue(minimumPower);
		};

		evcs.getFixedMaximumHardwarePowerChannel().onSetNextValue(calculateHardwarePowerLimits);
		evcs.getFixedMinimumHardwarePowerChannel().onSetNextValue(calculateHardwarePowerLimits);
		evcs.getPhasesChannel().onSetNextValue(calculateHardwarePowerLimits);
	}

	/**
	 * Adds listeners on the current channels to calculate the number of used
	 * {@link ChannelId#PHASES}.
	 *
	 * @param evcs the evcs
	 */
	public static void calculateUsedPhasesFromCurrent(Evcs evcs) {
		final Consumer<Value<Integer>> calculatePhases = ignore -> {
			var phases = evaluatePhaseCountFromCurrent(//
					evcs.getCurrentL1().get(), evcs.getCurrentL2().get(), evcs.getCurrentL3().get());
			if (phases == null) {
				/*
				 * Always set 3 phases in case of an unplugged/uncharging car. This is, because
				 * we should always start computation for new charging sessions based on 3
				 * phases.
				 */
				phases = 3;
			}
			evcs._setPhases(phases);
		};
		evcs.getCurrentL1Channel().onSetNextValue(calculatePhases);
		evcs.getCurrentL2Channel().onSetNextValue(calculatePhases);
		evcs.getCurrentL3Channel().onSetNextValue(calculatePhases);
	}

	/**
	 * Evaluates the number of Phases from the individual currents per phase.
	 *
	 * <p>
	 * The EVCS will pull power from the grid for its own consumption and report
	 * that on one of the phases. This value is different from EVCS to EVCS but can
	 * be high. Because of this, this will only register a phase starting with
	 * ~450mA because then we definitively know that this load is caused by a car.
	 *
	 * @param currentL1 current on L1 in mA
	 * @param currentL2 current on L2 in mA
	 * @param currentL3 current on L3 in mA
	 * @return integer value indicating the number of phases; null if undefined
	 */
	public static Integer evaluatePhaseCountFromCurrent(Integer currentL1, Integer currentL2, Integer currentL3) {
		var phases = 0;
		if (currentL1 != null && currentL1 > MIN_EVCS_ACTIVITY_CURRENT) {
			phases++;
		}
		if (currentL2 != null && currentL2 > MIN_EVCS_ACTIVITY_CURRENT) {
			phases++;
		}
		if (currentL3 != null && currentL3 > MIN_EVCS_ACTIVITY_CURRENT) {
			phases++;
		}
		return switch (phases) {
		case 1, 2, 3 -> phases;
		default -> null;
		};
	}

	/**
	 * Adds a listener to calculate the phase powers
	 * {@link ChannelId#ACTIVE_POWER_L1}, {@link ChannelId#ACTIVE_POWER_L2},
	 * {@link ChannelId#ACTIVE_POWER_L3} based on the {@link ChannelId#ACTIVE_POWER}
	 * active power and the phase currents {@link ChannelId#CURRENT_L1},
	 * {@link ChannelId#CURRENT_L2}, {@link ChannelId#CURRENT_L3}.
	 *
	 * <p/>
	 * Note:
	 * <ul>
	 * <li>This is different from
	 * {@link ElectricityMeter.calculatePhasesFromActivePower}}, which handles
	 * symmetric consumers only.</li>
	 * <li>if no active {@link ChannelId#PHASES} can be detected, it is set to 3 by
	 * default. This avoids sideeffects in some charge stations, when the charge
	 * process is started.</li>
	 * </ul>
	 * 
	 * @param evcs the evcs
	 */
	public static void calculatePhasesFromActivePowerAndPhaseCurrents(Evcs evcs) {
		final Consumer<Value<Integer>> calculatePhasePowers = ignore -> {
			var currentL1 = evcs.getCurrentL1().get();
			var currentL2 = evcs.getCurrentL2().get();
			var currentL3 = evcs.getCurrentL3().get();
			var phaseCnt = evaluatePhaseCountFromCurrent(currentL1, currentL2, currentL3);
			if (phaseCnt == null) {
				// assume three phases by default
				phaseCnt = 3;
			}
			var phasePower = divide(evcs.getActivePower().get(), phaseCnt);
			evcs._setActivePowerL1((currentL1 != null && currentL1 > MIN_EVCS_ACTIVITY_CURRENT) ? phasePower : 0);
			evcs._setActivePowerL2((currentL2 != null && currentL2 > MIN_EVCS_ACTIVITY_CURRENT) ? phasePower : 0);
			evcs._setActivePowerL3((currentL3 != null && currentL3 > MIN_EVCS_ACTIVITY_CURRENT) ? phasePower : 0);
		};
		evcs.getActivePowerChannel().onSetNextValue(calculatePhasePowers);
		evcs.getCurrentL1Channel().onSetNextValue(calculatePhasePowers);
		evcs.getCurrentL2Channel().onSetNextValue(calculatePhasePowers);
		evcs.getCurrentL3Channel().onSetNextValue(calculatePhasePowers);
	}

	/**
	 * Used for Modbus/TCP Api Controller. Provides a Modbus table for the Channels
	 * of this Component.
	 *
	 * @param accessMode filters the Modbus-Records that should be shown
	 * @return the {@link ModbusSlaveNatureTable}
	 */
	public static ModbusSlaveNatureTable getModbusSlaveNatureTable(AccessMode accessMode) {
		return ModbusSlaveNatureTable.of(Evcs.class, accessMode, 100) //
				.channel(0, ChannelId.STATUS, ModbusType.UINT16) //
				.uint16Reserved(1) //
				.channel(2, ChannelId.CHARGING_TYPE, ModbusType.UINT16) //
				.channel(3, ChannelId.PHASES, ModbusType.UINT16) //
				.channel(4, ChannelId.MAXIMUM_HARDWARE_POWER, ModbusType.UINT16) //
				.channel(5, ChannelId.MINIMUM_HARDWARE_POWER, ModbusType.UINT16) //
				.channel(6, ChannelId.MAXIMUM_POWER, ModbusType.UINT16) //
				.channel(7, ChannelId.ENERGY_SESSION, ModbusType.UINT16) //
				.channel(8, ChannelId.CHARGINGSTATION_COMMUNICATION_FAILED, ModbusType.UINT16) //
				.channel(9, ChannelId.FIXED_MINIMUM_HARDWARE_POWER, ModbusType.UINT16) //
				.channel(10, ChannelId.FIXED_MAXIMUM_HARDWARE_POWER, ModbusType.UINT16) //
				.channel(11, ChannelId.MINIMUM_POWER, ModbusType.UINT16) //
				.build();
	}

	/**
	 * Defines if the evcs is read only.
	 *
	 * @return true if the evcs is read-only
	 */
	public default boolean isReadOnly() {
		return false;
	}
}
