package io.openems.edge.solaredge.ess;

import static io.openems.common.utils.IntUtils.sumInteger;
import static io.openems.edge.bridge.modbus.api.ElementToChannelConverter.DIRECT_1_TO_1;
import static io.openems.edge.bridge.modbus.api.ElementToChannelConverter.SCALE_FACTOR_3;
import static io.openems.edge.common.channel.ChannelUtils.setValue;
import static io.openems.edge.common.event.EdgeEventConstants.TOPIC_CYCLE_BEFORE_PROCESS_IMAGE;
import static io.openems.edge.common.event.EdgeEventConstants.TOPIC_CYCLE_EXECUTE_WRITE;
import static java.time.temporal.ChronoUnit.MILLIS;
import static org.osgi.service.component.annotations.ReferenceCardinality.MANDATORY;
import static org.osgi.service.component.annotations.ReferenceCardinality.OPTIONAL;
import static org.osgi.service.component.annotations.ReferencePolicy.DYNAMIC;
import static org.osgi.service.component.annotations.ReferencePolicy.STATIC;
import static org.osgi.service.component.annotations.ReferencePolicyOption.GREEDY;

import java.time.LocalDateTime;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Stream;

import org.osgi.service.component.ComponentContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.ConfigurationPolicy;
import org.osgi.service.component.annotations.Deactivate;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.event.Event;
import org.osgi.service.event.EventHandler;
import org.osgi.service.event.propertytypes.EventTopics;
import org.osgi.service.metatype.annotations.Designate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;

import io.openems.common.channel.AccessMode;
import io.openems.common.exceptions.OpenemsError.OpenemsNamedException;
import io.openems.common.exceptions.OpenemsException;
import io.openems.common.referencetarget.GenerateTargetsFromReferences;
import io.openems.edge.bridge.modbus.api.BridgeModbus;
import io.openems.edge.bridge.modbus.api.ElementToChannelConverter;
import io.openems.edge.bridge.modbus.api.ModbusComponent;
import io.openems.edge.bridge.modbus.api.ModbusProtocol;
import io.openems.edge.bridge.modbus.api.element.DummyRegisterElement;
import io.openems.edge.bridge.modbus.api.element.FloatDoublewordElement;
import io.openems.edge.bridge.modbus.api.element.SignedDoublewordElement;
import io.openems.edge.bridge.modbus.api.element.UnsignedDoublewordElement;
import io.openems.edge.bridge.modbus.api.element.UnsignedQuadruplewordElement;
import io.openems.edge.bridge.modbus.api.element.UnsignedWordElement;
import io.openems.edge.bridge.modbus.api.element.WordOrder;
import io.openems.edge.bridge.modbus.api.task.FC16WriteRegistersTask;
import io.openems.edge.bridge.modbus.api.task.FC3ReadRegistersTask;
import io.openems.edge.bridge.modbus.sunspec.DefaultSunSpecModel;
import io.openems.edge.bridge.modbus.sunspec.DefaultSunSpecModel.S1;
import io.openems.edge.bridge.modbus.sunspec.DefaultSunSpecModel.S101;
import io.openems.edge.bridge.modbus.sunspec.DefaultSunSpecModel.S102;
import io.openems.edge.bridge.modbus.sunspec.DefaultSunSpecModel.S103;
import io.openems.edge.bridge.modbus.sunspec.FilteredSunSpecModel;
import io.openems.edge.bridge.modbus.sunspec.SunSpecModel;
import io.openems.edge.bridge.modbus.sunspec.ess.AbstractSunSpecEss;
import io.openems.edge.common.channel.FloatReadChannel;
import io.openems.edge.common.channel.IntegerReadChannel;
import io.openems.edge.common.channel.IntegerWriteChannel;
import io.openems.edge.common.channel.value.Value;
import io.openems.edge.common.component.ComponentManager;
import io.openems.edge.common.component.OpenemsComponent;
import io.openems.edge.common.cycle.Cycle;
import io.openems.edge.common.modbusslave.ModbusSlave;
import io.openems.edge.common.modbusslave.ModbusSlaveNatureTable;
import io.openems.edge.common.modbusslave.ModbusSlaveTable;
import io.openems.edge.common.sum.GridMode;
import io.openems.edge.common.sum.Sum;
import io.openems.edge.common.taskmanager.Priority;
import io.openems.edge.common.type.Phase.SingleOrAllPhase;
import io.openems.edge.common.type.Phase.SinglePhase;
import io.openems.edge.common.type.TypeUtils;
import io.openems.edge.ess.api.AsymmetricEss;
import io.openems.edge.ess.api.HybridEss;
import io.openems.edge.ess.api.ManagedAsymmetricEss;
import io.openems.edge.ess.api.ManagedSinglePhaseEss;
import io.openems.edge.ess.api.ManagedSymmetricEss;
import io.openems.edge.ess.api.SinglePhaseEss;
import io.openems.edge.ess.api.SymmetricEss;
import io.openems.edge.ess.power.api.Power;
import io.openems.edge.solaredge.charger.SolarEdgeCharger;
import io.openems.edge.timedata.api.Timedata;
import io.openems.edge.timedata.api.TimedataProvider;
import io.openems.edge.timedata.api.utils.CalculateEnergyFromPower;

@Designate(ocd = Config.class, factory = true)
@Component(//
		name = "SolarEdge.ESS", //
		immediate = true, //
		configurationPolicy = ConfigurationPolicy.REQUIRE //
)
@EventTopics({ //
		TOPIC_CYCLE_BEFORE_PROCESS_IMAGE, //
		TOPIC_CYCLE_EXECUTE_WRITE //
})
@GenerateTargetsFromReferences("Modbus")
public class SolarEdgeEssImpl extends AbstractSunSpecEss implements SolarEdgeEss, ManagedSinglePhaseEss, SinglePhaseEss,
		ManagedAsymmetricEss, AsymmetricEss, ManagedSymmetricEss, SymmetricEss, HybridEss, ModbusComponent,
		OpenemsComponent, EventHandler, ModbusSlave, TimedataProvider {

	protected static final SunSpecModel S_101_WITHOUT_EVENTS = FilteredSunSpecModel.withoutPoints(
			DefaultSunSpecModel.S_101, //
			DefaultSunSpecModel.S101.EVT1, DefaultSunSpecModel.S101.EVT2, DefaultSunSpecModel.S101.EVT_VND1,
			DefaultSunSpecModel.S101.EVT_VND2, DefaultSunSpecModel.S101.EVT_VND3, DefaultSunSpecModel.S101.EVT_VND4);

	protected static final SunSpecModel S_102_WITHOUT_EVENTS = FilteredSunSpecModel.withoutPoints(
			DefaultSunSpecModel.S_102, //
			DefaultSunSpecModel.S102.EVT1, DefaultSunSpecModel.S102.EVT2, DefaultSunSpecModel.S102.EVT_VND1,
			DefaultSunSpecModel.S102.EVT_VND2, DefaultSunSpecModel.S102.EVT_VND3, DefaultSunSpecModel.S102.EVT_VND4);

	protected static final SunSpecModel S_103_WITHOUT_EVENTS = FilteredSunSpecModel.withoutPoints(
			DefaultSunSpecModel.S_103, //
			DefaultSunSpecModel.S103.EVT1, DefaultSunSpecModel.S103.EVT2, DefaultSunSpecModel.S103.EVT_VND1,
			DefaultSunSpecModel.S103.EVT_VND2, DefaultSunSpecModel.S103.EVT_VND3, DefaultSunSpecModel.S103.EVT_VND4);

	private static enum InverterType {
		SINGLE_PHASE(S_101_WITHOUT_EVENTS), //
		SPLIT_PHASE(S_102_WITHOUT_EVENTS), //
		THREE_PHASE(S_103_WITHOUT_EVENTS);

		private final List<SunSpecModel> blocks;

		private InverterType(SunSpecModel... blocks) {
			this.blocks = Lists.newArrayList(blocks);
		}
	}

	private static final int READ_FROM_MODBUS_BLOCK = 1;

	private static final Map<SunSpecModel, Priority> ACTIVE_MODELS = ImmutableMap.<SunSpecModel, Priority>builder()
			.put(DefaultSunSpecModel.S_1, Priority.LOW) //
			.put(S_101_WITHOUT_EVENTS, Priority.HIGH) // S101 without Events: not supported by SolarEdge
			.put(S_102_WITHOUT_EVENTS, Priority.HIGH) // S102 without Events: not supported by SolarEdge
			.put(S_103_WITHOUT_EVENTS, Priority.HIGH) // S103 without Events: not supported by SolarEdge
			.build();

	protected final Set<SolarEdgeCharger> chargers = new HashSet<>();

	private final AllowedChargeDischargeHandler allowedChargeDischargeHandler = new AllowedChargeDischargeHandler(this);
	private final ApplyPowerHandler applyPowerHandler = new ApplyPowerHandler();
	private final SetPvExportLimitHandler setPvExportLimitHandler = new SetPvExportLimitHandler(this);

	private final Logger log = LoggerFactory.getLogger(SolarEdgeEss.class);

	private final ElementToChannelConverter ignoreMinPower = IgnoreMinPowerConverter.from(this, DIRECT_1_TO_1);

	private final CalculateEnergyFromPower calculateAcChargeEnergy = new CalculateEnergyFromPower(this,
			SymmetricEss.ChannelId.ACTIVE_CHARGE_ENERGY);
	private final CalculateEnergyFromPower calculateAcDischargeEnergy = new CalculateEnergyFromPower(this,
			SymmetricEss.ChannelId.ACTIVE_DISCHARGE_ENERGY);
	private final CalculateEnergyFromPower calculateDcChargeEnergy = new CalculateEnergyFromPower(this,
			HybridEss.ChannelId.DC_CHARGE_ENERGY);
	private final CalculateEnergyFromPower calculateDcDischargeEnergy = new CalculateEnergyFromPower(this,
			HybridEss.ChannelId.DC_DISCHARGE_ENERGY);

	@Reference(//
			policy = STATIC, policyOption = GREEDY, cardinality = MANDATORY, //
			target = "(&(id=${config.modbus_id})(enabled=true))")
	protected void setModbus(BridgeModbus modbus) {
		super.setModbus(modbus);
	}

	@Reference(policy = DYNAMIC, policyOption = GREEDY, cardinality = OPTIONAL)
	private volatile Timedata timedata = null;

	@Reference
	private Cycle cycle;

	@Reference
	private Power power;

	@Reference
	private Sum sum;

	@Reference
	private ComponentManager componentManager;

	private Config config;
	private SinglePhase singlePhase = null;
	private InverterType inverterType = null;
	private AverageCalculator pvProductionAverageCalculator;

	public SolarEdgeEssImpl() throws OpenemsNamedException {
		super(//
				ACTIVE_MODELS, //
				OpenemsComponent.ChannelId.values(), //
				ModbusComponent.ChannelId.values(), //
				HybridEss.ChannelId.values(), //
				SymmetricEss.ChannelId.values(), //
				ManagedSymmetricEss.ChannelId.values(), //
				AsymmetricEss.ChannelId.values(), //
				ManagedAsymmetricEss.ChannelId.values(), //
				SinglePhaseEss.ChannelId.values(), //
				ManagedSinglePhaseEss.ChannelId.values(), //
				SolarEdgeEss.ChannelId.values() //
		);
	}

	@Activate
	private void activate(ComponentContext context, Config config) throws OpenemsNamedException {
		this.config = config;
		super.activate(context, config.id(), config.alias(), config.enabled(), config.modbusUnitId(),
				READ_FROM_MODBUS_BLOCK);

		// Evaluate 'SinglePhase'
		switch (config.phase()) {
		case ALL -> this.singlePhase = null;
		case L1 -> this.singlePhase = SinglePhase.L1;
		case L2 -> this.singlePhase = SinglePhase.L2;
		case L3 -> this.singlePhase = SinglePhase.L3;
		}

		this.pvProductionAverageCalculator = new AverageCalculator(120 * 1000 / this.cycle.getCycleTime()); // 120s

		setValue(this, SymmetricEss.ChannelId.GRID_MODE, GridMode.ON_GRID);
		this.addStaticModbusTasks(this.getModbusProtocol());
	}

	@Override
	protected void onSunSpecInitializationCompleted() {
		this.logInfo(this.log, "SunSpec initialization finished. " + this.channels().size() + " Channels available.");

		this.channel(SolarEdgeEss.ChannelId.WRONG_PHASE_CONFIGURED).setNextValue(
				this.inverterType == InverterType.SINGLE_PHASE ? this.config.phase() == SingleOrAllPhase.ALL
						: this.config.phase() != SingleOrAllPhase.ALL);

		this.mapFirstPointToChannel(//
				SolarEdgeEss.ChannelId.SERIAL_NUMBER, //
				DIRECT_1_TO_1, //
				S1.SN);

		this.mapFirstPointToChannel(//
				SymmetricEss.ChannelId.ACTIVE_POWER, //
				this.ignoreMinPower, //
				S101.W, S102.W, S103.W);
		this.mapFirstPointToChannel(//
				SymmetricEss.ChannelId.REACTIVE_POWER, //
				DIRECT_1_TO_1, //
				S101.V_AR, S102.V_AR, S103.V_AR);
		this.mapFirstPointToChannel(//
				SolarEdgeEss.ChannelId.INVERTER_ACTIVE_DC_POWER, //
				DIRECT_1_TO_1, //
				S101.DCW, S102.DCW, S103.DCW);

		// Individual Phases Power
		switch (this.inverterType) {
		case SINGLE_PHASE -> {
			SolarEdgeEssImpl.calculateSinglePhaseFromActivePower(this, this.config.phase());
		}
		case SPLIT_PHASE, THREE_PHASE -> {
			SolarEdgeEssImpl.calculatePhasesFromActivePower(this);
			SolarEdgeEssImpl.calculatePhasesFromReactivePower(this);
		}
		}

		// PV Production Energy
		this.mapFirstPointToChannel(//
				SolarEdgeEss.ChannelId.ACTIVE_PRODUCTION_ENERGY, //
				DIRECT_1_TO_1, //
				S101.WH, S102.WH, S103.WH);

		// DC Voltage
		this.mapFirstPointToChannel(//
				SolarEdgeEss.ChannelId.VOLTAGE_DC, //
				SCALE_FACTOR_3, // Convert V to mV
				S101.DCV, S102.DCV, S103.DCV);
	}

	@Override
	protected void addBlock(int startAddress, SunSpecModel model, Priority priority) {
		super.addBlock(startAddress, model, priority);

		// Evaluate the InverterType from this Block
		Stream.of(InverterType.values()) //
				.filter(type -> type.blocks.stream().anyMatch(t -> t.equals(model))) //
				.findFirst() //
				.ifPresent(type -> this.inverterType = type);
	}

	@Override
	@Deactivate
	protected void deactivate() {
		super.deactivate();
	}

	/**
	 * Adds static modbus tasks.
	 * 
	 * @param protocol the {@link ModbusProtocol}
	 * @throws OpenemsException on error
	 */
	private void addStaticModbusTasks(ModbusProtocol protocol) throws OpenemsException {

		// StorEdge Control Block register
		protocol.addTask(//
				new FC3ReadRegistersTask(0xE004, Priority.LOW, //
						m(SolarEdgeEss.ChannelId.STORAGE_CONTROL_MODE, new UnsignedWordElement(0xE004)),
						m(SolarEdgeEss.ChannelId.STORAGE_AC_CHARGE_POLICY, new UnsignedWordElement(0xE005)),
						m(SolarEdgeEss.ChannelId.STORAGE_AC_CHARGE_LIMIT, new FloatDoublewordElement(0xE006) //
								.wordOrder(WordOrder.LSWMSW)),
						m(SolarEdgeEss.ChannelId.STORAGE_BACKUP_RESERVED_SETTING, new FloatDoublewordElement(0xE008) //
								.wordOrder(WordOrder.LSWMSW)),
						m(SolarEdgeEss.ChannelId.STORAGE_CHARGE_DISCHARGE_DEFAULT_MODE,
								new UnsignedWordElement(0xE00A)),
						m(SolarEdgeEss.ChannelId.REMOTE_CONTROL_COMMAND_TIMEOUT, new UnsignedDoublewordElement(0xE00B) //
								.wordOrder(WordOrder.LSWMSW)),
						m(SolarEdgeEss.ChannelId.REMOTE_CONTROL_COMMAND_MODE, new UnsignedWordElement(0xE00D)),
						m(SolarEdgeEss.ChannelId.REMOTE_CONTROL_COMMAND_CHARGE_LIMIT, new FloatDoublewordElement(0xE00E) //
								.wordOrder(WordOrder.LSWMSW)),
						m(SolarEdgeEss.ChannelId.REMOTE_CONTROL_COMMAND_DISCHARGE_LIMIT,
								new FloatDoublewordElement(0xE010) //
										.wordOrder(WordOrder.LSWMSW))));

		// StorEdge Battery 1 Status and Information Block register (Task 1)
		protocol.addTask(//
				new FC3ReadRegistersTask(0xE144, Priority.HIGH, //
						// feeds AllowedChargeDischargeHandler
						m(SolarEdgeEss.ChannelId.BATTERY1_MAX_CHARGE_CONTINUES_POWER, new FloatDoublewordElement(0xE144) //
								.wordOrder(WordOrder.LSWMSW)),
						// feeds llowedChargeDischargeHandler
						m(SolarEdgeEss.ChannelId.BATTERY1_MAX_DISCHARGE_CONTINUES_POWER,
								new FloatDoublewordElement(0xE146) //
										.wordOrder(WordOrder.LSWMSW)),
						m(SolarEdgeEss.ChannelId.BATTERY1_MAX_CHARGE_PEAK_POWER, //
								new FloatDoublewordElement(0xE148) //
										.wordOrder(WordOrder.LSWMSW)),
						m(SolarEdgeEss.ChannelId.BATTERY1_MAX_DISCHARGE_PEAK_POWER, //
								new FloatDoublewordElement(0xE14A) //
										.wordOrder(WordOrder.LSWMSW)),
						new DummyRegisterElement(0xE14C, 0xE16B), // Reserved
						m(SolarEdgeEss.ChannelId.BATTERY1_AVG_TEMPERATURE, //
								new FloatDoublewordElement(0xE16C) //
										.wordOrder(WordOrder.LSWMSW)),
						m(SolarEdgeEss.ChannelId.BATTERY1_MAX_TEMPERATURE, //
								new FloatDoublewordElement(0xE16E) //
										.wordOrder(WordOrder.LSWMSW)),
						m(SolarEdgeEss.ChannelId.BATTERY1_ACTUAL_VOLTAGE, //
								new FloatDoublewordElement(0xE170) //
										.wordOrder(WordOrder.LSWMSW)),
						// Instantaneous Battery Current (charge / discharge)
						m(SolarEdgeEss.ChannelId.BATTERY1_ACTUAL_CURRENT, new FloatDoublewordElement(0xE172) //
								.wordOrder(WordOrder.LSWMSW), SCALE_FACTOR_3 /* Convert A to mA */),
						// Instantaneous Battery Power (charge / discharge)
						m(SolarEdgeEss.ChannelId.BATTERY1_ACTUAL_POWER, new FloatDoublewordElement(0xE174) //
								.wordOrder(WordOrder.LSWMSW))));

		// StorEdge Battery 1 Status and Information Block register (Task 2)
		protocol.addTask(//
				new FC3ReadRegistersTask(0xE176, Priority.LOW,
						// Active Charge / Discharge energy valid for SolarEdge SE4000
						// For SolarEdge SE10K-RWS only valid until the next day/loading
						// cycle (not clear or verified)
						m(SolarEdgeEss.ChannelId.BATTERY1_LIFETIME_EXPORT_ENERGY, //
								new UnsignedQuadruplewordElement(0xE176) //
										.wordOrder(WordOrder.LSWMSW)),
						m(SolarEdgeEss.ChannelId.BATTERY1_LIFETIME_IMPORT_ENERGY, //
								new UnsignedQuadruplewordElement(0xE17A) //
										.wordOrder(WordOrder.LSWMSW)),
						m(SolarEdgeEss.ChannelId.BATTERY1_MAX_CAPACITY, //
								new FloatDoublewordElement(0xE17E) //
										.wordOrder(WordOrder.LSWMSW)),
						// Available capacity or "real" capacity
						m(SymmetricEss.ChannelId.CAPACITY, new FloatDoublewordElement(0xE180) //
								.wordOrder(WordOrder.LSWMSW)),
						m(SolarEdgeEss.ChannelId.SOH, //
								new FloatDoublewordElement(0xE182) //
										.wordOrder(WordOrder.LSWMSW)),
						m(SymmetricEss.ChannelId.SOC, //
								new FloatDoublewordElement(0xE184) //
										.wordOrder(WordOrder.LSWMSW)),
						m(SolarEdgeEss.ChannelId.BATTERY1_STATUS, //
								new UnsignedDoublewordElement(0xE186) //
										.wordOrder(WordOrder.LSWMSW))));

		// Smart Meter register
		protocol.addTask(//
				new FC3ReadRegistersTask(40121, Priority.LOW, //
						m(SolarEdgeEss.ChannelId.METER_COMMUNICATE_STATUS, new UnsignedWordElement(40121))));

		// Power Control Block Register
		protocol.addTask(//
				new FC3ReadRegistersTask(0xF140, Priority.LOW, //
						// Power Reduction (e.g. 70 %)
						m(SolarEdgeEss.ChannelId.INVERTER_POWER_LIMIT, new FloatDoublewordElement(0xF140) //
								.wordOrder(WordOrder.LSWMSW)),
						// AdvacedPwrControlEn
						m(SolarEdgeEss.ChannelId.ADVANCED_PWR_CONTROL_EN, new SignedDoublewordElement(0xF142) //
								.wordOrder(WordOrder.LSWMSW))));

		// Enhanced Power Control Block register
		protocol.addTask(//
				new FC3ReadRegistersTask(0xF304, Priority.LOW, //
						m(SolarEdgeEss.ChannelId.INVERTER_MAX_APPARENT_POWER, new FloatDoublewordElement(0xF304) //
								.wordOrder(WordOrder.LSWMSW))));

		// Export Limit Control Block register
		protocol.addTask(//
				new FC3ReadRegistersTask(0xE000, Priority.LOW, //
						m(SolarEdgeEss.ChannelId.EXPORT_CONTROL_MODE, new UnsignedWordElement(0xE000)),
						m(SolarEdgeEss.ChannelId.EXPORT_CONTROL_LIMIT_MODE, new UnsignedWordElement(0xE001)),
						m(SolarEdgeEss.ChannelId.EXPORT_CONTROL_SITE_LIMIT, new FloatDoublewordElement(0xE002) //
								.wordOrder(WordOrder.LSWMSW))));

		// StorEdge Control Block register
		protocol.addTask(//
				new FC16WriteRegistersTask(0xE00B,
						m(SolarEdgeEss.ChannelId.REMOTE_CONTROL_COMMAND_TIMEOUT, new UnsignedDoublewordElement(0xE00B) //
								.wordOrder(WordOrder.LSWMSW)),
						m(SolarEdgeEss.ChannelId.REMOTE_CONTROL_COMMAND_MODE, new UnsignedWordElement(0xE00D)),
						m(SolarEdgeEss.ChannelId.REMOTE_CONTROL_COMMAND_CHARGE_LIMIT, new FloatDoublewordElement(0xE00E) //
								.wordOrder(WordOrder.LSWMSW)),
						m(SolarEdgeEss.ChannelId.REMOTE_CONTROL_COMMAND_DISCHARGE_LIMIT,
								new FloatDoublewordElement(0xE010) //
										.wordOrder(WordOrder.LSWMSW))));

		// Export Limit Control Block register
		protocol.addTask(//
				new FC16WriteRegistersTask(0xE002, //
						m(SolarEdgeEss.ChannelId.EXPORT_CONTROL_SITE_LIMIT, new FloatDoublewordElement(0xE002) //
								.wordOrder(WordOrder.LSWMSW))));

	}

	@Override
	public void applyPower(int activePower, int reactivePower) throws OpenemsNamedException {
		// Apply Power Set-Point
		this.applyPowerHandler.apply(this, activePower, this.config.controlMode(), this.sum.getGridActivePower(),
				this.getActivePower(), this.power.isFilterEnabled());
	}

	@Override
	public void applyPower(int activePowerL1, int reactivePowerL1, int activePowerL2, int reactivePowerL2,
			int activePowerL3, int reactivePowerL3) throws OpenemsNamedException {
		if (this.config.phase() == SingleOrAllPhase.ALL) {
			return;
		}

		ManagedSinglePhaseEss.super.applyPower(activePowerL1, reactivePowerL1, activePowerL2, reactivePowerL2,
				activePowerL3, reactivePowerL3);
	}

	@Override
	public void handleEvent(Event event) {
		if (!this.isEnabled()) {
			return;
		}

		switch (event.getTopic()) {
		case TOPIC_CYCLE_BEFORE_PROCESS_IMAGE -> {
			this.calculateAndSetActualPvPower();
			this.allowedChargeDischargeHandler.accept(this.componentManager);
			this.updateMaxApparentPowerChannel();
			this.updateDcDischargePowerChannel();
			this.updateEnergyChannels();
		}
		case TOPIC_CYCLE_EXECUTE_WRITE -> {
			this.applyPvExportLimit();
		}
		}
	}

	/**
	 * Calculates Actual PV Power out of Total DC Power and Battery Charge/Discharge
	 * Power.
	 */
	public void calculateAndSetActualPvPower() {
		try {
			final IntegerReadChannel activeDcPowerChannel = this
					.channel(SolarEdgeEss.ChannelId.INVERTER_ACTIVE_DC_POWER);
			final IntegerReadChannel batteryPowerChannel = this.channel(SolarEdgeEss.ChannelId.BATTERY1_ACTUAL_POWER);

			/*
			 * Get the DC Power value from 1 second ago
			 * 
			 * Since the inverter has to retrieve the battery power value from the SESTI
			 * interface, this is outdated. For the PV production calculation to be
			 * accurate, it is assumed that the value is received with a 1-second delay.
			 * Therefore, we must add the battery power to the DCW value from 1 second ago
			 * to obtain the PV power.
			 * 
			 */
			var cycleTimeSeconds = Math.max(1, this.cycle.getCycleTime() / 1000); // minimum 1 second
			var retrievePastSeconds = Math.min(cycleTimeSeconds, 1) * 2; // twice the cycle time or 2*1 second
			var pastActiveDcPowerValues = activeDcPowerChannel.getPastValues().tailMap(
					LocalDateTime.now(this.componentManager.getClock()).minusSeconds(retrievePastSeconds), true); //

			LocalDateTime target = batteryPowerChannel.getNextValue().getTimestamp().minusSeconds(1);
			int dcPower = this.findCloseRecord(pastActiveDcPowerValues.values(), target).getOrError();

			// Calculate the actual PV power and add it to the average values ​​of the
			// calculator
			int lastPvPower = dcPower + batteryPowerChannel.getNextValue().getOrError();
			this.pvProductionAverageCalculator.addValue(lastPvPower > 0 ? lastPvPower : 0);

			// Get the actual PV power as an average
			int pvProduction = ignoreImpossibleMinPower(this.pvProductionAverageCalculator.getAverage());

			// Write the PV power into the actual power channel of our DC charger
			for (SolarEdgeCharger charger : this.chargers) {
				charger.getActualPowerChannel().setNextValue(pvProduction);
			}
		} catch (Exception e) {
			return;
		}
	}

	/**
	 * Find record in list closest to dateTime.
	 * 
	 * @param list     the list to search in
	 * @param dateTime the dateTime to which the searched record should be closest
	 * @return listEntry
	 */
	public Value<Integer> findCloseRecord(Collection<Value<Integer>> list, LocalDateTime dateTime) {
		if (list == null || list.isEmpty()) {
			return null;
		}

		return list.stream().min(Comparator.comparingLong(d -> Math.abs(MILLIS.between(dateTime, d.getTimestamp()))))
				.orElse(null);
	}

	/**
	 * Ignore impossible minimum power.
	 * 
	 * <p>
	 * Even if there is no real power from PV, the Inverter Power Channel could
	 * remain on minimum power values. These values are ignored.
	 * 
	 * @param pvProduction the pvProduction
	 * @return pvProduction or zero
	 */
	protected static Integer ignoreImpossibleMinPower(Integer pvProduction) {
		if (pvProduction == null) {
			return pvProduction;
		}

		return pvProduction < 50 /* W */ ? 0 : pvProduction;
	}

	/**
	 * Send PV Export Site Limit to Inverter.
	 */
	public void applyPvExportLimit() {
		// Get ActiveExportPowerLimit that should be applied
		var activeExportPowerLimitChannel = (IntegerWriteChannel) this
				.channel(SolarEdgeEss.ChannelId.ACTIVE_EXPORT_POWER_LIMIT);
		var activeExportPowerLimitOpt = activeExportPowerLimitChannel.getNextWriteValueAndReset();

		// Set warning if pvExportLimit mode is disabled but a PV export limit was
		// requested
		this.channel(SolarEdgeEss.ChannelId.DISABLED_PV_EXPORT_LIMIT_FAILED)
				.setNextValue(!this.config.pvExportLimit() && activeExportPowerLimitOpt.isPresent());

		// If pvExportLimit mode is disabled: stop here
		if (!this.config.pvExportLimit()) {
			return;
		}

		try {
			this.setPvExportLimitHandler.accept(activeExportPowerLimitOpt);

			this.channel(SolarEdgeEss.ChannelId.PV_EXPORT_LIMIT_FAILED).setNextValue(false);
		} catch (OpenemsNamedException e) {
			this.channel(SolarEdgeEss.ChannelId.PV_EXPORT_LIMIT_FAILED).setNextValue(true);
		}
	}

	@Override
	public String debugLog() {
		return "SoC:" + this.getSoc().asString() //
				+ "|L:" + this.getActivePower().asString() //
				+ "|Allowed:" + this.getAllowedChargePower().asStringWithoutUnit() + ";" //
				+ this.getAllowedDischargePower().asString() //
				+ "|" + this.getGridModeChannel().value().asOptionString();
	}

	@Override
	public Power getPower() {
		return this.power;
	}

	@Override
	public int getPowerPrecision() {
		return 1;
	}

	@Override
	public SinglePhase getPhase() {
		return this.singlePhase;
	}

	@Override
	public Timedata getTimedata() {
		return this.timedata;
	}

	@Override
	public Integer getSurplusPower() {
		// TODO logic is insufficient
		if (this.getSoc().orElse(0) < 99) {
			return null;
		}
		var productionPower = this.getPvProduction();
		if (productionPower == null || productionPower < 100) {
			return null;
		}
		return productionPower;
		// return productionPower + 200 /* discharge more than PV production to avoid PV
		// curtail */;
	}

	@Override
	public ModbusSlaveTable getModbusSlaveTable(AccessMode accessMode) {
		return new ModbusSlaveTable(//
				OpenemsComponent.getModbusSlaveNatureTable(accessMode), //
				SymmetricEss.getModbusSlaveNatureTable(accessMode), //
				HybridEss.getModbusSlaveNatureTable(accessMode), //
				ModbusSlaveNatureTable.of(SolarEdgeEssImpl.class, accessMode, 100) //
						.build());
	}

	@Override
	public void addCharger(SolarEdgeCharger charger) {
		this.chargers.add(charger);
	}

	@Override
	public void removeCharger(SolarEdgeCharger charger) {
		this.chargers.remove(charger);
	}

	@Override
	public String getModbusBridgeId() {
		return this.config.modbus_id();
	}

	@Override
	protected void logInfo(Logger log, String message) {
		super.logInfo(log, message);
	}

	/**
	 * Gets the PV production from chargers ACTUAL_POWER. Returns null if the PV
	 * production is not available.
	 *
	 * @return production power
	 *
	 */
	public Integer getPvProduction() {
		Integer productionPower = null;
		for (SolarEdgeCharger charger : this.chargers) {
			productionPower = sumInteger(productionPower, charger.getActualPower().get());
		}
		return productionPower;
	}

	protected void updateMaxApparentPowerChannel() {

		/*
		 * Fill MaxApparentPower Channel
		 */
		FloatReadChannel inverterMaxApparentPowerChannel = this
				.channel(SolarEdgeEss.ChannelId.INVERTER_MAX_APPARENT_POWER);
		var inverterMaxApparentPower = inverterMaxApparentPowerChannel.value();
		var inverterPowerLimit = this.getInverterPowerLimit();
		if (inverterMaxApparentPower != null && inverterPowerLimit != null) {
			this._setMaxApparentPower(
					Math.round(TypeUtils.multiply(inverterMaxApparentPower.get(), inverterPowerLimit.get(), 0.01f)));
		}
	}

	protected void updateDcDischargePowerChannel() {
		try {
			/*
			 * Fill DcDischargePower Channel Reverse calculation from PV production as
			 * battery power values are outdated, because the inverter has to retrieve the
			 * value from the SESTI interface.
			 */
			final IntegerReadChannel activeDcPowerChannel = this
					.channel(SolarEdgeEss.ChannelId.INVERTER_ACTIVE_DC_POWER);

			// PV-Production
			Integer pvProduction = 0;
			for (SolarEdgeCharger charger : this.chargers) {
				pvProduction = sumInteger(pvProduction, charger.getActualPowerChannel().getNextValue().getOrError());
			}

			var dcBatteryActualPower = activeDcPowerChannel.getNextValue().getOrError() - pvProduction;
			this._setDcDischargePower(Math.abs(dcBatteryActualPower) > 50 ? dcBatteryActualPower : 0);
		} catch (Exception e) {
			return;
		}
	}

	protected void updateEnergyChannels() {

		/*
		 * Calculate AC Energy
		 */
		var acActivePower = this.getActivePower().get();
		if (acActivePower == null) {
			// Not available
			this.calculateAcChargeEnergy.update(null);
			this.calculateAcDischargeEnergy.update(null);
		} else if (acActivePower > 0) {
			// Discharge
			this.calculateAcChargeEnergy.update(0);
			this.calculateAcDischargeEnergy.update(acActivePower);
		} else {
			// Charge
			this.calculateAcChargeEnergy.update(acActivePower * -1);
			this.calculateAcDischargeEnergy.update(0);
		}

		/*
		 * Calculate DC Energy
		 */
		var dcDischargePower = this.getDcDischargePower().get();
		if (dcDischargePower == null) {
			// Not available
			this.calculateDcChargeEnergy.update(null);
			this.calculateDcDischargeEnergy.update(null);
		} else if (dcDischargePower > 0) {
			// Discharge
			this.calculateDcChargeEnergy.update(0);
			this.calculateDcDischargeEnergy.update(dcDischargePower);
		} else {
			// Charge
			this.calculateDcChargeEnergy.update(dcDischargePower * -1);
			this.calculateDcDischargeEnergy.update(0);
		}
	}

	/**
	 * Sets the correct value for ACTIVE_POWER_L1, ACTIVE_POWER_L2 or
	 * ACTIVE_POWER_L3-Channel from ACTIVE_POWER.
	 * 
	 * @param solarEdge the SolarEdgeEssImpl instance
	 * @param phase     the phase to which the inverter is connected
	 */
	public static void calculateSinglePhaseFromActivePower(SolarEdgeEssImpl solarEdge, SingleOrAllPhase phase) {
		solarEdge.getActivePowerChannel().onSetNextValue(value -> {
			// Fallback to L1 on wrong configuration
			setValue(solarEdge, AsymmetricEss.ChannelId.ACTIVE_POWER_L1,
					phase == SingleOrAllPhase.L1 || phase == SingleOrAllPhase.ALL ? value : null);
			setValue(solarEdge, AsymmetricEss.ChannelId.ACTIVE_POWER_L2, phase == SingleOrAllPhase.L2 ? value : null);
			setValue(solarEdge, AsymmetricEss.ChannelId.ACTIVE_POWER_L3, phase == SingleOrAllPhase.L3 ? value : null);
		});
	}

	/**
	 * Calculate the ACTIVE_POWER_L1, ACTIVE_POWER_L2 and ACTIVE_POWER_L3-Channels
	 * from ACTIVE_POWER by dividing by three.
	 * 
	 * @param solarEdge the SolarEdgeEssImpl instance
	 */
	public static void calculatePhasesFromActivePower(SolarEdgeEssImpl solarEdge) {
		solarEdge.getActivePowerChannel().onSetNextValue(value -> {
			var phase = TypeUtils.divide(value.get(), 3);
			setValue(solarEdge, AsymmetricEss.ChannelId.ACTIVE_POWER_L1, phase);
			setValue(solarEdge, AsymmetricEss.ChannelId.ACTIVE_POWER_L2, phase);
			setValue(solarEdge, AsymmetricEss.ChannelId.ACTIVE_POWER_L3, phase);
		});
	}

	/**
	 * Calculate the REACTIVE_POWER_L1, REACTIVE_POWER_L2 and
	 * REACTIVE_POWER_L3-Channels from REACTIVE_POWER by dividing by three.
	 * 
	 * @param solarEdge the SolarEdgeEssImpl instance
	 */
	public static void calculatePhasesFromReactivePower(SolarEdgeEssImpl solarEdge) {
		solarEdge.getReactivePowerChannel().onSetNextValue(value -> {
			var phase = TypeUtils.divide(value.get(), 3);
			setValue(solarEdge, AsymmetricEss.ChannelId.REACTIVE_POWER_L1, phase);
			setValue(solarEdge, AsymmetricEss.ChannelId.REACTIVE_POWER_L2, phase);
			setValue(solarEdge, AsymmetricEss.ChannelId.REACTIVE_POWER_L3, phase);
		});
	}

}
