From 1c993f27f1f8acc40efba630d70b6dfdc33cba5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Mon, 9 Mar 2026 22:48:06 +0100 Subject: [PATCH 1/4] energy: Add simulated wallbox witout meter - Prevent duplicate wallbox assignments - Reset charge session timestamp on unplug - Clean up car and wallbox links on removal --- .../integrationpluginenergysimulation.cpp | 281 +++++++++++++++--- .../integrationpluginenergysimulation.h | 12 +- .../integrationpluginenergysimulation.json | 105 +++++++ 3 files changed, 346 insertions(+), 52 deletions(-) diff --git a/energysimulation/integrationpluginenergysimulation.cpp b/energysimulation/integrationpluginenergysimulation.cpp index fe76dc1..990a103 100644 --- a/energysimulation/integrationpluginenergysimulation.cpp +++ b/energysimulation/integrationpluginenergysimulation.cpp @@ -3,7 +3,7 @@ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Copyright (C) 2013 - 2024, nymea GmbH -* Copyright (C) 2024 - 2025, chargebyte austria GmbH +* Copyright (C) 2024 - 2026, chargebyte austria GmbH * * This file is part of nymea-plugins-simulation. * @@ -24,22 +24,20 @@ #include "integrationpluginenergysimulation.h" -#include "plugintimer.h" #include "plugininfo.h" +#include "plugintimer.h" #include #define CIVIL_ZENITH 90.83333 - -IntegrationPluginEnergySimulation::IntegrationPluginEnergySimulation(QObject *parent): IntegrationPlugin (parent) -{ - -} +IntegrationPluginEnergySimulation::IntegrationPluginEnergySimulation(QObject *parent) + : IntegrationPlugin(parent) +{} void IntegrationPluginEnergySimulation::discoverThings(ThingDiscoveryInfo *info) { - QTimer::singleShot(1000, info, [=]{ + QTimer::singleShot(1000, info, [=] { ThingClass thingClass = IntegrationPlugin::thingClass(info->thingClassId()); for (uint i = 0; i < configValue(energySimulationPluginDiscoveryResultCountParamTypeId).toUInt(); i++) { ThingDescriptor descriptor(info->thingClassId(), thingClass.displayName()); @@ -60,13 +58,13 @@ void IntegrationPluginEnergySimulation::setupThing(ThingSetupInfo *info) } if (thing->thingClassId() == wallboxThingClassId) { - connect(info->thing(), &Thing::settingChanged, this, [this, thing](const ParamTypeId &settingTypeId, const QVariant &value){ + connect(info->thing(), &Thing::settingChanged, this, [this, thing](const ParamTypeId &settingTypeId, const QVariant &value) { if (settingTypeId == wallboxSettingsMaxChargingCurrentUpperLimitParamTypeId) { thing->setStateMaxValue(wallboxMaxChargingCurrentStateTypeId, value); } if (settingTypeId == wallboxSettingsPhaseParamTypeId) { if (value.toString() == "All") { - thing->setStatePossibleValues(wallboxDesiredPhaseCountStateTypeId, {1,3}); + thing->setStatePossibleValues(wallboxDesiredPhaseCountStateTypeId, {1, 3}); } else { thing->setStatePossibleValues(wallboxDesiredPhaseCountStateTypeId, {1}); } @@ -75,6 +73,23 @@ void IntegrationPluginEnergySimulation::setupThing(ThingSetupInfo *info) }); } + if (thing->thingClassId() == wallboxNoMeterThingClassId) { + connect(info->thing(), &Thing::settingChanged, this, [this, thing](const ParamTypeId &settingTypeId, const QVariant &value) { + if (settingTypeId == wallboxNoMeterSettingsMaxChargingCurrentUpperLimitParamTypeId) { + thing->setStateMaxValue(wallboxNoMeterMaxChargingCurrentStateTypeId, value); + } + if (settingTypeId == wallboxNoMeterSettingsPhaseParamTypeId) { + if (value.toString() == "All") { + thing->setStatePossibleValues(wallboxNoMeterDesiredPhaseCountStateTypeId, {1, 3}); + } else { + thing->setStatePossibleValues(wallboxNoMeterDesiredPhaseCountStateTypeId, {1}); + } + } + updateSimulation(); + }); + } + + if (thing->thingClassId() == stoveThingClassId) { // Init property for simulation thing->setProperty("simulationActive", false); @@ -84,11 +99,10 @@ void IntegrationPluginEnergySimulation::setupThing(ThingSetupInfo *info) } if (thing->thingClassId() == genericCarThingClassId) { - thing->setStateValue(genericCarPhaseCountStateTypeId, thing->setting(genericCarSettingsPhaseCountParamTypeId)); thing->setStateValue(genericCarCapacityStateTypeId, thing->setting(genericCarSettingsCapacityParamTypeId)); - connect(info->thing(), &Thing::settingChanged, this, [thing](const ParamTypeId &settingTypeId, const QVariant &value){ + connect(info->thing(), &Thing::settingChanged, this, [thing](const ParamTypeId &settingTypeId, const QVariant &value) { if (settingTypeId == genericCarSettingsPhaseCountParamTypeId) { thing->setStateValue(genericCarPhaseCountStateTypeId, value); } else if (settingTypeId == genericCarSettingsCapacityParamTypeId) { @@ -98,10 +112,25 @@ void IntegrationPluginEnergySimulation::setupThing(ThingSetupInfo *info) } } - void IntegrationPluginEnergySimulation::thingRemoved(Thing *thing) { - Q_UNUSED(thing) + if (thing->thingClassId() == apiCarThingClassId || thing->thingClassId() == genericCarThingClassId) { + foreach (Thing *wallbox, myThings().filterByThingClassId(wallboxThingClassId)) { + unplugCarFromWallbox(wallbox, thing->id()); + } + foreach (Thing *wallbox, myThings().filterByThingClassId(wallboxNoMeterThingClassId)) { + unplugCarFromWallbox(wallbox, thing->id()); + } + return; + } + + if (thing->thingClassId() == wallboxThingClassId || thing->thingClassId() == wallboxNoMeterThingClassId) { + Thing *car = myThings().findById(thing->property("connectedCarThingId").toUuid()); + if (car) { + car->setStateValue("pluggedIn", false); + car->setProperty("lastChargeUpdateTime", QDateTime()); + } + } } void IntegrationPluginEnergySimulation::executeAction(ThingActionInfo *info) @@ -131,18 +160,58 @@ void IntegrationPluginEnergySimulation::executeAction(ThingActionInfo *info) info->thing()->setStateValue(wallboxDesiredPhaseCountStateTypeId, desiredPhaseCount); } } + + if (info->thing()->thingClassId() == wallboxNoMeterThingClassId) { + if (info->action().actionTypeId() == wallboxNoMeterPowerActionTypeId) { + info->thing()->setStateValue(wallboxNoMeterPowerStateTypeId, info->action().paramValue(wallboxNoMeterPowerActionPowerParamTypeId).toBool()); + } + if (info->action().actionTypeId() == wallboxNoMeterMaxChargingCurrentActionTypeId) { + info->thing()->setStateValue(wallboxNoMeterMaxChargingCurrentStateTypeId, info->action().paramValue(wallboxNoMeterMaxChargingCurrentActionMaxChargingCurrentParamTypeId)); + } + if (info->action().actionTypeId() == wallboxNoMeterConnectActionTypeId) { + info->thing()->setStateValue(wallboxNoMeterConnectedStateTypeId, true); + } + if (info->action().actionTypeId() == wallboxNoMeterDisconnectActionTypeId) { + info->thing()->setStateValue(wallboxNoMeterConnectedStateTypeId, false); + } + + if (info->action().actionTypeId() == wallboxNoMeterDesiredPhaseCountActionTypeId) { + uint desiredPhaseCount = info->action().paramValue(wallboxNoMeterDesiredPhaseCountActionDesiredPhaseCountParamTypeId).toInt(); + qCDebug(dcEnergySimulation()) << "Setting desired phase count to" << desiredPhaseCount; + info->thing()->setStateValue(wallboxNoMeterDesiredPhaseCountStateTypeId, desiredPhaseCount); + } + } + if (info->thing()->thingClassId() == apiCarThingClassId || info->thing()->thingClassId() == genericCarThingClassId) { if (info->action().actionTypeId() == apiCarPluggedInActionTypeId || info->action().actionTypeId() == genericCarPluggedInActionTypeId) { ParamTypeId pluggedInParamTypeId = info->thing()->thingClass().actionTypes().findByName("pluggedIn").paramTypes().findByName("pluggedIn").id(); if (info->action().paramValue(pluggedInParamTypeId).toBool()) { + foreach (Thing *wallbox, myThings().filterByThingClassId(wallboxThingClassId)) { + if (wallbox->property("connectedCarThingId").toUuid() == info->thing()->id()) { + info->thing()->setStateValue("pluggedIn", true); + info->finish(Thing::ThingErrorNoError); + return; + } + } + foreach (Thing *wallbox, myThings().filterByThingClassId(wallboxNoMeterThingClassId)) { + if (wallbox->property("connectedCarThingId").toUuid() == info->thing()->id()) { + info->thing()->setStateValue("pluggedIn", true); + info->finish(Thing::ThingErrorNoError); + return; + } + } + // Try to plug the car to the first free wallbox foreach (Thing *wallbox, myThings().filterByThingClassId(wallboxThingClassId)) { - if (wallbox->property("connectedCarThingId").toUuid().isNull()) { - // Found an empty wallbox, plugging it in - wallbox->setProperty("connectedCarThingId", info->thing()->id()); + if (plugCarIntoWallbox(wallbox, info->thing())) { + info->thing()->setStateValue("pluggedIn", true); + info->finish(Thing::ThingErrorNoError); + return; + } + } + foreach (Thing *wallbox, myThings().filterByThingClassId(wallboxNoMeterThingClassId)) { + if (plugCarIntoWallbox(wallbox, info->thing())) { info->thing()->setStateValue("pluggedIn", true); - wallbox->setStateValue(wallboxPluggedInStateTypeId, true); - wallbox->setStateValue(wallboxSessionEnergyStateTypeId, 0); info->finish(Thing::ThingErrorNoError); return; } @@ -154,13 +223,16 @@ void IntegrationPluginEnergySimulation::executeAction(ThingActionInfo *info) info->thing()->setStateValue("pluggedIn", false); // Unplug from wallbox foreach (Thing *wallbox, myThings().filterByThingClassId(wallboxThingClassId)) { - if (wallbox->property("connectedCarThingId").toUuid() == info->thing()->id()) { - wallbox->setProperty("connectedCarThingId", QUuid()); - wallbox->setStateValue(wallboxPluggedInStateTypeId, false); - wallbox->setStateValue(wallboxSessionEnergyStateTypeId, 0); + if (unplugCarFromWallbox(wallbox, info->thing()->id())) { break; } } + foreach (Thing *wallbox, myThings().filterByThingClassId(wallboxNoMeterThingClassId)) { + if (unplugCarFromWallbox(wallbox, info->thing()->id())) { + break; + } + } + info->thing()->setProperty("lastChargeUpdateTime", QDateTime()); info->finish(Thing::ThingErrorNoError); return; } @@ -194,7 +266,7 @@ void IntegrationPluginEnergySimulation::updateSimulation() qCDebug(dcEnergySimulation()) << "******************* Adjusting simulation" << QDateTime::currentDateTime().toString(); // Update solar inverters - foreach (Thing* inverter, myThings().filterByThingClassId(solarInverterThingClassId)) { + foreach (Thing *inverter, myThings().filterByThingClassId(solarInverterThingClassId)) { QDateTime now = QDateTime::currentDateTime(); int hoursOffset = inverter->setting(solarInverterSettingsHoursOffsetParamTypeId).toInt(); now = now.addSecs(hoursOffset * 60 * 60); @@ -222,7 +294,7 @@ void IntegrationPluginEnergySimulation::updateSimulation() } // Update evchargers - foreach (Thing* evCharger, myThings().filterByThingClassId(wallboxThingClassId)) { + foreach (Thing *evCharger, myThings().filterByThingClassId(wallboxThingClassId)) { if (evCharger->stateValue(wallboxPluggedInStateTypeId).toBool() && evCharger->stateValue(wallboxPowerStateTypeId).toBool()) { ThingId connectedCarThingId = evCharger->property("connectedCarThingId").toUuid(); Thing *car = myThings().findById(connectedCarThingId); @@ -244,9 +316,8 @@ void IntegrationPluginEnergySimulation::updateSimulation() double chargingPower = 230 * maxChargingCurrent * phaseCount; double chargingTimeHours = 1.0 * lastChargeUpdateTime.msecsTo(QDateTime::currentDateTime()) / 1000 / 60 / 60; double chargedWattHours = chargingPower * chargingTimeHours; - double carCapacity = car->stateValue("capacityState").toDouble(); // cWH : cap = x : 100 - double chargedPercentage = chargedWattHours / 1000 * 100 / carCapacity; + double chargedPercentage = calculateChargedPercentage(car, chargedWattHours); qCDebug(dcEnergySimulation()) << "* #### Car charging info:"; qCDebug(dcEnergySimulation()) << "* # max charging current:" << maxChargingCurrent << "A on" << phaseCount << "phases"; qCDebug(dcEnergySimulation()) << "* # time passed since last update:" << chargingTimeHours; @@ -284,6 +355,65 @@ void IntegrationPluginEnergySimulation::updateSimulation() } } + foreach (Thing *evCharger, myThings().filterByThingClassId(wallboxNoMeterThingClassId)) { + if (evCharger->stateValue(wallboxNoMeterPluggedInStateTypeId).toBool() && evCharger->stateValue(wallboxNoMeterPowerStateTypeId).toBool()) { + ThingId connectedCarThingId = evCharger->property("connectedCarThingId").toUuid(); + Thing *car = myThings().findById(connectedCarThingId); + qCDebug(dcEnergySimulation()) << "* Evaluating wallbox (no meter):" << evCharger->name() << "Connected car:" << (car ? car->name() : "none"); + if (car && car->stateValue("batteryLevel").toInt() < 100) { + evCharger->setStateValue(wallboxNoMeterChargingStateTypeId, true); + QDateTime lastChargeUpdateTime = car->property("lastChargeUpdateTime").toDateTime(); + if (lastChargeUpdateTime.isNull()) { + car->setProperty("lastChargeUpdateTime", QDateTime::currentDateTime()); + break; + } + double maxChargingCurrent = evCharger->stateValue(wallboxNoMeterMaxChargingCurrentStateTypeId).toDouble(); + uint connectedPhaseCount = evCharger->setting(wallboxNoMeterSettingsPhaseParamTypeId).toString() == "All" ? 3 : 1; + uint desiredPhaseCount = evCharger->stateValue(wallboxNoMeterDesiredPhaseCountStateTypeId).toUInt(); + uint carPhaseCount = car->hasState("phaseCount") ? car->stateValue("phaseCount").toUInt() : 1; + qCDebug(dcEnergySimulation()) << "Connected phases:" << connectedPhaseCount << "desired phases:" << desiredPhaseCount << "Car phases:" << carPhaseCount; + uint phaseCount = qMin(desiredPhaseCount, qMin(connectedPhaseCount, carPhaseCount)); + evCharger->setStateValue(wallboxNoMeterPhaseCountStateTypeId, phaseCount); + + double chargingPower = 230 * maxChargingCurrent * phaseCount; + double chargingTimeHours = 1.0 * lastChargeUpdateTime.msecsTo(QDateTime::currentDateTime()) / 1000 / 60 / 60; + double chargedWattHours = chargingPower * chargingTimeHours; + // cWH : cap = x : 100 + double chargedPercentage = calculateChargedPercentage(car, chargedWattHours); + qCDebug(dcEnergySimulation()) << "* #### Car charging info:"; + qCDebug(dcEnergySimulation()) << "* # max charging current:" << maxChargingCurrent << "A on" << phaseCount << "phases"; + qCDebug(dcEnergySimulation()) << "* # time passed since last update:" << chargingTimeHours; + qCDebug(dcEnergySimulation()) << "* # charged" << chargedWattHours << "Wh," << chargedPercentage << "%"; + + //evCharger->setStateValue(wallboxCurrentPowerStateTypeId, chargingPower); + evCharger->setProperty("currentPower", chargingPower); + double totalEnergyConsumed = evCharger->property("totalEnergyConsumed").toDouble(); + double chargedEnergy = (chargingPower / 1000) / 60 / 60 * 5; + totalEnergyConsumed += chargedEnergy; + + qCDebug(dcEnergySimulation()) << "* # total:" << totalEnergyConsumed << "kWh"; + evCharger->setProperty("totalEnergyConsumed", totalEnergyConsumed); + + if (car->thingClassId() == apiCarThingClassId) { + if (chargedPercentage >= 1) { + car->setProperty("lastChargeUpdateTime", QDateTime::currentDateTime()); + + car->setStateValue("batteryLevel", car->stateValue("batteryLevel").toInt() + chargedPercentage); + car->setStateValue("batteryCritical", car->stateValue("batteryLevel").toInt() < 10); + } + } + } else { + qCDebug(dcEnergySimulation()) << "* Ev charger using 0 (Car already full)"; + evCharger->setStateValue(wallboxNoMeterChargingStateTypeId, false); + evCharger->setProperty("currentPower", 0); + } + } else { + qCDebug(dcEnergySimulation()) << "* Ev charger using 0 (Car not plugged in or charging disabled)"; + evCharger->setStateValue(wallboxNoMeterChargingStateTypeId, false); + evCharger->setProperty("currentPower", 0); + } + } + // Reduce battery level on all unplugged cars foreach (Thing *car, myThings().filterByInterface("electricvehicle")) { if (!car->stateValue("pluggedIn").toBool() && car->stateValue("batteryLevel").toInt() > 0) { @@ -366,7 +496,7 @@ void IntegrationPluginEnergySimulation::updateSimulation() uint maxConsumption = heatPump->setting(sgReadyHeatPumpSettingsMaxConsumptionParamTypeId).toUInt(); double currentPower = 0; if (operatingMode == "Off") { - currentPower = 10 + (std::rand() % 5); // We need some energy since only the pump is off, not the controller + currentPower = 10 + (std::rand() % 5); // We need some energy since only the pump is off, not the controller } else if (operatingMode == "Low") { currentPower = minConsumption + (std::rand() % 20); } else if (operatingMode == "Standard") { @@ -411,18 +541,12 @@ void IntegrationPluginEnergySimulation::updateSimulation() heatPump->setStateValue(simpleHeatPumpTotalEnergyConsumedStateTypeId, totalEnergyConsumed); } - ///////////////////////////////////////////////////// /// Energy meter //////////////////////////////////////////////////// - // Sum up production from inverters - QHash totalPhaseProduction = { - {"A", 0}, - {"B", 0}, - {"C", 0} - }; + QHash totalPhaseProduction = {{"A", 0}, {"B", 0}, {"C", 0}}; foreach (Thing *inverter, myThings().filterByThingClassId(solarInverterThingClassId)) { QString phase = inverter->setting(solarInverterSettingsPhaseParamTypeId).toString(); double production = inverter->stateValue(solarInverterCurrentPowerStateTypeId).toDouble(); @@ -435,13 +559,8 @@ void IntegrationPluginEnergySimulation::updateSimulation() } } - // Sum up consumption of all consumers - QHash totalPhasesConsumption = { - {"A", 0}, - {"B", 0}, - {"C", 0} - }; + QHash totalPhasesConsumption = {{"A", 0}, {"B", 0}, {"C", 0}}; // Simulate a base consumption of 300W (100 on each phase) + 10W jitter totalPhasesConsumption["A"] += 100 + (std::rand() % 10); totalPhasesConsumption["B"] += 100 + (std::rand() % 10); @@ -464,6 +583,22 @@ void IntegrationPluginEnergySimulation::updateSimulation() } } + // Also add the wallbox (no meter) values, they still consume energy even if they cannot show it on their own. + foreach (Thing *evCharger, myThings().filterByThingClassId(wallboxNoMeterThingClassId)) { + double currentPower = evCharger->property("currentPower").toDouble(); + QString phase = evCharger->setting("phase").toString(); + + if (phase == "All" || "ABC") { + qCDebug(dcEnergySimulation()) << "Adding" << currentPower / 3 << "per phase for" << evCharger->name(); + totalPhasesConsumption["A"] += currentPower / 3; + totalPhasesConsumption["B"] += currentPower / 3; + totalPhasesConsumption["C"] += currentPower / 3; + } else { + qCDebug(dcEnergySimulation()) << "Adding" << currentPower << "to phase" << phase << "for" << evCharger->name(); + totalPhasesConsumption[phase] += currentPower; + } + } + // Sum up all phases for the total consumption/production (momentary, in Watt) double totalProduction = 0; foreach (double phaseProduction, totalPhaseProduction) { @@ -475,7 +610,6 @@ void IntegrationPluginEnergySimulation::updateSimulation() } double grandTotal = totalConsumption + totalProduction; // Note: production is negative - // Charge/discharge batteries depending on totals so far foreach (Thing *battery, myThings().filterByThingClassId(batteryThingClassId)) { int batteryLevel = battery->stateValue(batteryBatteryLevelStateTypeId).toInt(); @@ -521,7 +655,6 @@ void IntegrationPluginEnergySimulation::updateSimulation() battery->setStateValue(batteryChargingStateStateTypeId, "discharging"); battery->setStateValue(batteryCurrentPowerStateTypeId, -returnedWatts); - double pendingDischargedWh = battery->property("pendingDischargedWh").toDouble(); QDateTime lastUpdate = battery->property("lastUpdate").toDateTime(); double hoursSinceLastUpdate = 1.0 * lastUpdate.msecsTo(QDateTime::currentDateTime()) / 1000 / 60 / 60; @@ -542,7 +675,6 @@ void IntegrationPluginEnergySimulation::updateSimulation() } } - // Sum up all phases *again* after the battery has been facored in totalProduction = 0; foreach (double phaseProduction, totalPhaseProduction) { @@ -588,6 +720,59 @@ void IntegrationPluginEnergySimulation::updateSimulation() } } +bool IntegrationPluginEnergySimulation::plugCarIntoWallbox(Thing *wallbox, Thing *car) +{ + if (!wallbox->property("connectedCarThingId").toUuid().isNull()) { + return false; + } + + wallbox->setProperty("connectedCarThingId", car->id()); + + if (wallbox->thingClassId() == wallboxThingClassId) { + wallbox->setStateValue(wallboxPluggedInStateTypeId, true); + wallbox->setStateValue(wallboxSessionEnergyStateTypeId, 0); + } else if (wallbox->thingClassId() == wallboxNoMeterThingClassId) { + wallbox->setStateValue(wallboxNoMeterPluggedInStateTypeId, true); + } + + return true; +} + +bool IntegrationPluginEnergySimulation::unplugCarFromWallbox(Thing *wallbox, const ThingId &carId) +{ + if (wallbox->property("connectedCarThingId").toUuid() != carId) { + return false; + } + + wallbox->setProperty("connectedCarThingId", QUuid()); + + if (wallbox->thingClassId() == wallboxThingClassId) { + wallbox->setStateValue(wallboxPluggedInStateTypeId, false); + wallbox->setStateValue(wallboxSessionEnergyStateTypeId, 0); + } else if (wallbox->thingClassId() == wallboxNoMeterThingClassId) { + wallbox->setStateValue(wallboxNoMeterPluggedInStateTypeId, false); + } + + return true; +} + +double IntegrationPluginEnergySimulation::carCapacity(Thing *car) +{ + return car->stateValue("capacity").toDouble(); +} + +double IntegrationPluginEnergySimulation::calculateChargedPercentage(Thing *car, double chargedWattHours) +{ + const double capacity = carCapacity(car); + if (capacity <= 0) { + qCWarning(dcEnergySimulation()) << "Skipping battery percentage update for" << car->name() + << "because capacity is invalid:" << capacity; + return 0; + } + + return chargedWattHours / 1000 * 100 / capacity; +} + QPair IntegrationPluginEnergySimulation::calculateSunriseSunset(qreal latitude, qreal longitude, const QDateTime &dateTime) { int dayOfYear = dateTime.date().dayOfYear(); @@ -615,12 +800,12 @@ QPair IntegrationPluginEnergySimulation::calculateSunriseS qreal raSet = qRound(tmp + 360) % 360 + (tmp - qFloor(tmp)); // Right ascension value needs to be in the same quadrant as L - qlonglong lQuadrantRise = qFloor(lRise/90) * 90; - qlonglong raQuadrantRise = qFloor(raRise/90) * 90; + qlonglong lQuadrantRise = qFloor(lRise / 90) * 90; + qlonglong raQuadrantRise = qFloor(raRise / 90) * 90; raRise = raRise + (lQuadrantRise - raQuadrantRise); - qlonglong lQuadrantSet = qFloor(lSet/90) * 90; - qlonglong raQuadrantSet = qFloor(raSet/90) * 90; + qlonglong lQuadrantSet = qFloor(lSet / 90) * 90; + qlonglong raQuadrantSet = qFloor(raSet / 90) * 90; raSet = raSet + (lQuadrantSet - raQuadrantSet); // Right ascension value needs to be converted into hours diff --git a/energysimulation/integrationpluginenergysimulation.h b/energysimulation/integrationpluginenergysimulation.h index 48400fd..1d7a7f8 100644 --- a/energysimulation/integrationpluginenergysimulation.h +++ b/energysimulation/integrationpluginenergysimulation.h @@ -3,7 +3,7 @@ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * Copyright (C) 2013 - 2024, nymea GmbH -* Copyright (C) 2024 - 2025, chargebyte austria GmbH +* Copyright (C) 2024 - 2026, chargebyte austria GmbH * * This file is part of nymea-plugins-simulation. * @@ -25,12 +25,12 @@ #ifndef INTEGRATIONPLUGINENERGYSIMULATION_H #define INTEGRATIONPLUGINENERGYSIMULATION_H -#include "integrations/integrationplugin.h" #include "extern-plugininfo.h" +#include "integrations/integrationplugin.h" class PluginTimer; -class IntegrationPluginEnergySimulation: public IntegrationPlugin +class IntegrationPluginEnergySimulation : public IntegrationPlugin { Q_OBJECT @@ -50,12 +50,16 @@ private slots: void updateSimulation(); private: + bool plugCarIntoWallbox(Thing *wallbox, Thing *car); + bool unplugCarFromWallbox(Thing *wallbox, const ThingId &carId); + double carCapacity(Thing *car); + double calculateChargedPercentage(Thing *car, double chargedWattHours); + QPair calculateSunriseSunset(qreal latitude, qreal longitude, const QDateTime &dateTime); private: PluginTimer *m_timer = nullptr; PluginTimer *m_totalsTimer = nullptr; - }; #endif // INTEGRATIONPLUGINENERGYSIMULATION_H diff --git a/energysimulation/integrationpluginenergysimulation.json b/energysimulation/integrationpluginenergysimulation.json index be937fb..c2bedd3 100644 --- a/energysimulation/integrationpluginenergysimulation.json +++ b/energysimulation/integrationpluginenergysimulation.json @@ -146,6 +146,111 @@ } ] }, + { + "name": "wallboxNoMeter", + "displayName": "Simulated wallbox (no meter)", + "id": "d26123e6-71f4-4cfe-bc61-5709be2293f0", + "createMethods": ["discovery", "user"], + "interfaces": ["evcharger", "connectable"], + "settingsTypes": [ + { + "id": "70b0efec-c12e-426f-98f8-0aa22c84380b", + "name": "phase", + "displayName": "Connected on phase", + "type": "QString", + "allowedValues": ["A", "B", "C", "All"], + "defaultValue": "All" + }, + { + "id": "cc86d42b-4287-4cf4-b55e-a57cbbf330af", + "name": "maxChargingCurrentUpperLimit", + "displayName": "Upper limit for maximum charging current", + "type": "double", + "unit": "Ampere", + "defaultValue": 63 + } + ], + "stateTypes": [ + { + "id": "52222a35-e682-4329-8162-331295706cef", + "name": "connected", + "displayName": "Connected", + "type": "bool", + "defaultValue": true + }, + { + "id": "a88a2c35-4799-43c7-a11a-cc9f4e46b678", + "name": "power", + "displayName": "Charging enabled", + "displayNameAction": "Enable/disable charging", + "type": "bool", + "defaultValue": true, + "writable": true + }, + { + "id": "5bc3f079-7bba-4781-963c-992d3550c169", + "name": "maxChargingCurrent", + "displayName": "Maximum charging current", + "displayNameAction": "Set maximum charging current", + "type": "double", + "defaultValue": 6, + "minValue": 6, + "maxValue": 32, + "stepSize": 1.0, + "unit": "Ampere", + "writable": true + }, + { + "id": "9271e719-905e-4a6e-9dd5-1c9eb99dc252", + "name": "pluggedIn", + "displayName": "Car is plugged in", + "type": "bool", + "defaultValue": false, + "cached": false + }, + { + "id": "6c300adb-dc5c-4cd4-8a4a-5642ab58c2d7", + "name": "charging", + "displayName": "Charging", + "type": "bool", + "defaultValue": false, + "cached": false + }, + { + "id": "b68460fa-d31f-4f1a-831e-5a3ad4dfba60", + "name": "phaseCount", + "displayName": "Active phases", + "type": "uint", + "minValue": 1, + "maxValue": 3, + "defaultValue": 1 + }, + { + "id": "70ef11a0-e416-4160-be10-2dedfe3394fa", + "name": "desiredPhaseCount", + "displayName": "Desired phase count", + "displayNameAction": "Set desired phase count", + "type": "uint", + "minValue": 1, + "maxValue": 3, + "possibleValues": [1, 3], + "defaultValue": 3, + "writable": true + } + ], + "actionTypes": [ + { + "id": "1310ac86-e93c-427e-aa67-1d91491f2d98", + "name": "connect", + "displayName": "Connect wallbox" + }, + { + "id": "ef413aed-c15b-434b-afd4-f85a26a8a62b", + "name": "disconnect", + "displayName": "Disconnect wallbox" + } + ] + }, { "name": "solarInverter", "displayName": "Simulated solar inverter", From 715ba208684dc15421a7269109fc1aab3edb6a5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Mon, 9 Mar 2026 22:54:29 +0100 Subject: [PATCH 2/4] energy: Fix SG Ready mode state update --- energysimulation/integrationpluginenergysimulation.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/energysimulation/integrationpluginenergysimulation.cpp b/energysimulation/integrationpluginenergysimulation.cpp index 990a103..04fbf63 100644 --- a/energysimulation/integrationpluginenergysimulation.cpp +++ b/energysimulation/integrationpluginenergysimulation.cpp @@ -246,7 +246,7 @@ void IntegrationPluginEnergySimulation::executeAction(ThingActionInfo *info) if (info->thing()->thingClassId() == sgReadyHeatPumpThingClassId) { if (info->action().actionTypeId() == sgReadyHeatPumpSgReadyModeActionTypeId) { QString operatingMode = info->action().paramValue(sgReadyHeatPumpSgReadyModeActionSgReadyModeParamTypeId).toString(); - info->thing()->setStateValue(sgReadyHeatPumpSgReadyModeActionTypeId, operatingMode); + info->thing()->setStateValue(sgReadyHeatPumpSgReadyModeStateTypeId, operatingMode); } } else if (info->thing()->thingClassId() == simpleHeatPumpThingClassId) { if (info->action().actionTypeId() == simpleHeatPumpPowerActionTypeId) { From 13b4bf4aa8bfe430f689f48907305d1d66e463f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Mon, 9 Mar 2026 22:55:29 +0100 Subject: [PATCH 3/4] energy: Fix per-phase energy aggregation checks --- energysimulation/integrationpluginenergysimulation.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/energysimulation/integrationpluginenergysimulation.cpp b/energysimulation/integrationpluginenergysimulation.cpp index 04fbf63..3a8b531 100644 --- a/energysimulation/integrationpluginenergysimulation.cpp +++ b/energysimulation/integrationpluginenergysimulation.cpp @@ -571,7 +571,7 @@ void IntegrationPluginEnergySimulation::updateSimulation() if (consumer->thingClass().interfaces().contains("smartmeterconsumer")) { QString phase = consumer->setting("phase").toString(); double currentPower = consumer->stateValue("currentPower").toDouble(); - if (phase == "All" || "ABC") { + if (phase == "All" || phase == "ABC") { qCDebug(dcEnergySimulation()) << "Adding" << currentPower / 3 << "per phase for" << consumer->name(); totalPhasesConsumption["A"] += currentPower / 3; totalPhasesConsumption["B"] += currentPower / 3; @@ -588,7 +588,7 @@ void IntegrationPluginEnergySimulation::updateSimulation() double currentPower = evCharger->property("currentPower").toDouble(); QString phase = evCharger->setting("phase").toString(); - if (phase == "All" || "ABC") { + if (phase == "All" || phase == "ABC") { qCDebug(dcEnergySimulation()) << "Adding" << currentPower / 3 << "per phase for" << evCharger->name(); totalPhasesConsumption["A"] += currentPower / 3; totalPhasesConsumption["B"] += currentPower / 3; From c0fc04d9fd9a4cdb446b04d06ae0eb6389ca8a9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Tue, 10 Mar 2026 13:32:26 +0100 Subject: [PATCH 4/4] energy: Set stepSize of the simulated wallbox charging current to 0.1 Ampere --- energysimulation/integrationpluginenergysimulation.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/energysimulation/integrationpluginenergysimulation.json b/energysimulation/integrationpluginenergysimulation.json index c2bedd3..3f57df9 100644 --- a/energysimulation/integrationpluginenergysimulation.json +++ b/energysimulation/integrationpluginenergysimulation.json @@ -67,7 +67,7 @@ "defaultValue": 6, "minValue": 6, "maxValue": 32, - "stepSize": 1.0, + "stepSize": 0.1, "unit": "Ampere", "writable": true },