[smartics : charging smart]

In German there is this saying – freely translated: ‘firstly it will be different, and secondly than you think’. The translation might not fit it so well but in general the idea is that even when you know it will come differently, the outcome will quite often be even more different than predicted. This, of course, could be said about many specific fields but I think it fits very well into Software development. In engineering or development it has, due to the deterministic characteristics of the matter, a quite smug touch.

Why do I start with linguistic samples? We’ll see as I lead you through the smart charging code I scrambled together between being a little caught up by work, the flu and things just functioning differently than expected. Just like I do on high temperature.

So do worry, the code is not nice although I’ve gone over it already again. It might be a nice example to show some refactoring and how to test and refactor code in one of the next posts. But I also went over the settings topic again and prepared some setup and descriptions already – so this might also be one of the next topics. Not to say I am eager to also get some historical charging data and finally visualize some statistics – oh! where to start.

I already did a post about the charger repository here, and about the scheduler here, so there is not so much work for having a simple smart charging logic actually. Let me start with where we enter: the updated Scheduler. I used the one each five minutes as this will allow a smoother charging and should take care about spikes in a good enough way. By spikes I mean for example cloudy weather leading to short but high energy drops.

@Component
public class Scheduler {
	@Autowired
	private ChargerRepository chargerRepository;

	/**
	 * rough scheduler every 5 minutes.
	 * - fixedRate = 300000
	 */
	@Scheduled(cron = "0 */5 * * * *")
	public void roughScheduler() {
		final Instant currentTime = Instant.now();
		final Setting rough = settingService.findByName(SettingName.SCHEDULE_ROUGH);
		MeteringDataMin meteringData = null;

		// write metering data if available
		if (rough != null && rough.getValue() != null && rough.getValue().equals("true")) {
			LOG.info("ROUGH scheduler run at {}", InverterDateTimeFormater.getTimeReadableFormatted(currentTime));

			// check if data was not already written (has to be older than 4.5 mins)
			MeteringDataMin latestData = meteringDataMinService.getLatest();
			if (latestData != null && latestData.getUntilTime().toInstant().isAfter(currentTime.minus(230, ChronoUnit.SECONDS))) {
				LOG.info("rough: ran already, skip summarizing!");
				return;
			}

			// option: calculate during archived detail data
			List<MeteringDataSec> list;
			long diffLatestSecs = latestData != null
					? latestData.getUntilTime().toInstant().until(currentTime, ChronoUnit.SECONDS)
					: 0;
			// use latest entry if roughly 5 mins ago
			if (diffLatestSecs < 330 && diffLatestSecs > 230) {
				list = meteringDataSecService.findAllSince(latestData.getUntilTime().toInstant());
			} else {
				LOG.warn("rough: no latest entry roughly 5 mins ago found using all within last 5 mins!");
				list = meteringDataSecService.findForLastMinutes(currentTime, 5);
			}

			// option: no detail data available - get from realtime data
			if (list.isEmpty()) {
				LOG.info("rough: no detail data found, getting realtime data..");
				InverterDto inverterDto = inverterRepository.getRealtimeData();
				// minus 5 mins starttime if only one realtime entry exists, not using inverter time for following runs compatibility
				Instant correctStart = currentTime.minus(5, ChronoUnit.MINUTES);
				MeteringDataSec startMeteringData = new MeteringDataSec(Timestamp.from(correctStart), BigDecimal.ZERO,
						BigDecimal.ZERO, BigDecimal.ZERO, InverterStatus.OK.getCode());
				list.add(startMeteringData);
				MeteringDataSec meteringDataSec = InverterRealtimeMapper.convertToEntity(inverterDto);
				meteringDataSec.setCreationTime(Timestamp.from(currentTime));
				list.add(meteringDataSec);
			}
			meteringData = InverterCalculatorUtil.calculateSumForSecEntries(list);

			// warning on non 5-min spans
			meteringData.setStatusCode(MeteringDataMinService.getStatusForMeteringData(meteringData, list.size()).getCode());

			meteringDataMinService.save(meteringData);
			LOG.info("rough saved: {} produced, {} consumed, {} feedback!", meteringData.getPowerProduced(),
					meteringData.getPowerConsumed(), meteringData.getPowerFeedback());
		}

		// do smart charging if enabled
		Setting chargerMode = settingService.findByName(SettingName.CHARGER_MODE);
		if (chargerMode != null && meteringData != null) {
			chargerRepository.analyzeChargerStatus(chargerMode, meteringData);
		}
	}

So while here it was mostly just adding the charger repository and using the already gotten metering data for a call to it in line 66, I did need a few additional setting properties for charging mode, current amperage and voltage. This was the thought so part. The settings part can be found on Github, I will not go into details here for now. I’ll show the enum for the charging mode and then jump onto the problems with the actual charging logic.

public enum ChargerMode {
	/**
	 * charger not available
	 */
	UNAVAILABLE,
	/**
	 * no charging
	 */
	DEACTIVATED,
	/**
	 * charger set to fixed ampere
	 */
	FIXED,
	/**
	 * charger automatically retrieves ampere
	 */
	SMART;

	public static ChargerMode getByCode(String code) {
		if (code == null) {
			return ChargerMode.UNAVAILABLE;
		}
	    for (ChargerMode state : values()) {
	        if (state.name().equals(code)) {
	            return state;
	        }
	    }
	    return null;
	}
}

This was quick and painless. Let’s change that. What started as a small method in the Scheduler ended up rather complex (only due to plenty of conditions) with the need to get split up further in the charger repository. Due to the missing option to store a mode of charging on the charger itself, I temporarily added a priority setting which was bound to fail because I could not get around not getting the actual status of the charger before deciding on what to do or calculate anyway. I do think it is better to play it save here and query the charger. As this is done only every five minutes it will be fine.

@Repository
public class ChargerRepositoryImpl implements ChargerRepository {
	public static final int AMPERE_MIN = 6;
	public static final int COLOR_YELLOW = 16776960;
	public static final int COLOR_MAGENTA = 16711935;
	public static final int COLOR_BLUE = 65535;

	@Autowired
	private SettingService settingService;

	@Override
	public void analyzeChargerStatus(Setting settingChargerMode, final MeteringDataMin meteringData) {
		ChargerMode chargerMode = ChargerMode.getByCode(settingChargerMode.getValue());
		// check availability
		final ChargerStatusDto chargerStatus = getStatusData();
		if (ChargerMode.UNAVAILABLE == chargerMode) {
			LOG.info("analyzeChargerStatus: last charger state was not available, rechecking..");
			if (chargerStatus == null) {
				LOG.info("analyzeChargerStatus: charger still unavailable!");
				return;
			} else {
				chargerMode = ChargerMode.DEACTIVATED;
			}
		}
		Setting settingChargerAmp = settingService.findByName(SettingName.CHARGER_AMPERE_CURRENT);

		if (ChargerMode.SMART == chargerMode) {
			// calculate ampere and color for charging
			final Integer currentAmpere = calculateSmartCharging(chargerStatus, meteringData);
			if (currentAmpere != null) {
				settingChargerAmp.setValue(String.valueOf(currentAmpere));
				settingService.save(settingChargerAmp);
			}
		} else {
			// update charger state from charger
			if (chargerStatus == null) {
				settingChargerMode.setValue(ChargerMode.UNAVAILABLE.name());
			} else if (!chargerStatus.getAllowCharging().booleanValue()) {
				settingChargerMode.setValue(ChargerMode.DEACTIVATED.name());
			} else if (chargerStatus.getAllowCharging().booleanValue()) {
				settingChargerMode.setValue(ChargerMode.FIXED.name());
				settingChargerAmp.setValue(chargerStatus.getAmpere().toString());
				settingService.save(settingChargerAmp);
			}
			LOG.info("analyzeChargerStatus: no smart charging, charger status is {}", settingChargerMode.getValue());
			settingService.save(settingChargerMode);
		}
	}

	/**
	 * calculate and set smart charging data.
	 * 
	 * @param chargerStatus
	 * @param meteringData
	 * @return current ampere number
	 */
	protected Integer calculateSmartCharging(final ChargerStatusDto chargerStatus, final MeteringDataMin meteringData) {
		double excessEnergyWh = meteringData.getPowerFeedback().doubleValue() * 12;
		double extendEnergyWh = InverterCalculatorUtil.calcPowerFromNetwork(meteringData.getPowerConsumed(),
				meteringData.getPowerProduced(), meteringData.getPowerFeedback()) * 12;
		if (excessEnergyWh <= 0 && ChargerStatus.LOADING != chargerStatus.getConnectionStatus()) {
			LOG.info("analyzeChargerStatus: no excess energy available for smart charging!");
			return null;
		}
		// devide by Volt
		Setting settingChargerVolt = settingService.findByName(SettingName.CHARGER_VOLTAGE);
		if (settingChargerVolt == null || settingChargerVolt.getValue().isEmpty()) {
			LOG.error("analyzeChargerStatus: voltage has not been specified for charger, no smart charging!");
			return null;
		}
		double excessAmpere = excessEnergyWh / Integer.parseInt(settingChargerVolt.getValue());
		double extendAmpere = extendEnergyWh / Integer.parseInt(settingChargerVolt.getValue());
		LOG.info("analyzeChargerStatus: smart excess energy {} Wh, {} A", excessEnergyWh, excessAmpere);
		LOG.info("analyzeChargerStatus: smart extend energy {} Wh, {} A", extendEnergyWh, extendAmpere);
		// set color, activation.. if at least 2A excess
		if (excessAmpere > 2) {
			int ampereToSet = (int) excessAmpere;
			if (chargerStatus.getMaxAmpere() <= excessAmpere) {
				ampereToSet = chargerStatus.getMaxAmpere();
			}
			if (ChargerRepositoryImpl.AMPERE_MIN > ampereToSet) {
				ampereToSet = ChargerRepositoryImpl.AMPERE_MIN;
			}
			LOG.info("analyzeChargerStatus: smart usage of {} A", ampereToSet);
			// set color
			int colorCharging = ampereToSet <= excessAmpere ? ChargerRepositoryImpl.COLOR_YELLOW : ChargerRepositoryImpl.COLOR_MAGENTA;
			setAmpere(String.valueOf(ampereToSet));
			setColorCharging(String.valueOf(colorCharging));
			setAllowCharging(true);
			return ampereToSet;
		} else if (extendAmpere > 0) {
			// reduce charging
			if (chargerStatus.getAmpere() > 0 && ChargerStatus.LOADING == chargerStatus.getConnectionStatus()) {
				int ampereToSet = chargerStatus.getAmpere();
				if (extendAmpere >= ampereToSet || ampereToSet - extendAmpere <= ChargerRepositoryImpl.AMPERE_MIN) {
					// deactivate
					LOG.info("analyzeChargerStatus: smart usage deactivate");
					ampereToSet = 0;
					setAllowCharging(false);
				} else {
					ampereToSet = (int) (ampereToSet - extendAmpere);
					LOG.info("analyzeChargerStatus: smart usage reduced to {} A", ampereToSet);
					setAmpere(String.valueOf(ampereToSet));
				}
				return ampereToSet;
			}
		}
		return null;
	}
}

Here I left out the get and set methods I already showed in the post about the charger. What can be seen if this is compared to the plan I made about the smart charging? I did not think about the fact that without the current ampere stored I do not know if the currently consumed energy includes some from the charger. This was followed by me missing the fact that I do not care as long as I do not have to change the ampere number. And this was followed again by missing the point that if the excess energy actually drops, there is the need for extra logic to reduce the amperage by the extending energy (see line 71 and 72). That, of course, with all its minimum and maximum handling was more complicated than I thought. Even though I did not touch logic to take past data (like seasons and usual usage of the charger) into account yet.

All in all I will add something about testing and code refactoring, as without, this seems hardly maintainable as soon as some additional data should be taken into account.

— Raphael


Leave a comment