feat: ManualStrategy — community tier scheduling with full manual control

- Add ManualSlotConfig type with weekly repeating support and auto-expiry
- Add ManualStrategy (strategyId="manual"): applies user-defined allocations
  exactly; expired slots logged and skipped; inflexible/critical loads always
  applied as safety fallback; decisionReason never empty
- Extend SchedulerSettings with manualSlots persistence section (INI array)
- Extend SchedulerManager with setManualSlot/removeManualSlot/clearManualSlots
  methods; hydrates ManualStrategy from settings on registerStrategy()
- Add JSON-RPC v11 methods: GetManualSlots, SetManualSlot, RemoveManualSlot,
  ClearManualSlots + ManualSlotActivated push notification
- Register ManualStrategy in energypluginnymea.cpp::init() (no feature flag)
- Add 5 unit tests: basicSlot, noConfig_fallback, expiredSlot, repeatingSlot,
  persistence (JSON round-trip)
- Update doc.md section 11

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Patrick Schurig 2026-02-24 06:34:31 +01:00
parent 253c5f487a
commit 0797f37c78
30 changed files with 1568 additions and 111 deletions

View File

@ -65,12 +65,14 @@ SOURCES = ../../energyplugin/energymanagerconfiguration.cpp \
../../energyplugin/spotmarket/spotmarketmanager.cpp \
../../energyplugin/schedulingstrategies/rulebasedstrategy.cpp \
../../energyplugin/schedulingstrategies/aistrategy.cpp \
../../energyplugin/schedulingstrategies/manualstrategy.cpp \
../../energyplugin/types/chargingaction.cpp \
../../energyplugin/types/charginginfo.cpp \
../../energyplugin/types/chargingprocessinfo.cpp \
../../energyplugin/types/chargingschedule.cpp \
../../energyplugin/types/energytimeslot.cpp \
../../energyplugin/types/flexibleload.cpp \
../../energyplugin/types/manualslotconfig.cpp \
../../energyplugin/types/schedulerconfig.cpp \
../../energyplugin/types/scoreentry.cpp \
../../energyplugin/types/smartchargingstate.cpp \
@ -88,6 +90,7 @@ SOURCES = ../../energyplugin/energymanagerconfiguration.cpp \
moc_ischedulingstrategy.cpp \
moc_rulebasedstrategy.cpp \
moc_aistrategy.cpp \
moc_manualstrategy.cpp \
moc_chargingaction.cpp \
moc_charginginfo.cpp \
moc_chargingschedule.cpp \
@ -107,12 +110,14 @@ OBJECTS = energymanagerconfiguration.o \
spotmarketmanager.o \
rulebasedstrategy.o \
aistrategy.o \
manualstrategy.o \
chargingaction.o \
charginginfo.o \
chargingprocessinfo.o \
chargingschedule.o \
energytimeslot.o \
flexibleload.o \
manualslotconfig.o \
schedulerconfig.o \
scoreentry.o \
smartchargingstate.o \
@ -131,6 +136,7 @@ OBJECTS = energymanagerconfiguration.o \
moc_ischedulingstrategy.o \
moc_rulebasedstrategy.o \
moc_aistrategy.o \
moc_manualstrategy.o \
moc_chargingaction.o \
moc_charginginfo.o \
moc_chargingschedule.o \
@ -279,12 +285,14 @@ DIST = /usr/lib/x86_64-linux-gnu/qt6/mkspecs/features/spec_pre.prf \
../../energyplugin/schedulingstrategies/ischedulingstrategy.h \
../../energyplugin/schedulingstrategies/rulebasedstrategy.h \
../../energyplugin/schedulingstrategies/aistrategy.h \
../../energyplugin/schedulingstrategies/manualstrategy.h \
../../energyplugin/types/chargingaction.h \
../../energyplugin/types/charginginfo.h \
../../energyplugin/types/chargingprocessinfo.h \
../../energyplugin/types/chargingschedule.h \
../../energyplugin/types/energytimeslot.h \
../../energyplugin/types/flexibleload.h \
../../energyplugin/types/manualslotconfig.h \
../../energyplugin/types/schedulerconfig.h \
../../energyplugin/types/scoreentry.h \
../../energyplugin/types/smartchargingstate.h \
@ -302,12 +310,14 @@ DIST = /usr/lib/x86_64-linux-gnu/qt6/mkspecs/features/spec_pre.prf \
../../energyplugin/spotmarket/spotmarketmanager.cpp \
../../energyplugin/schedulingstrategies/rulebasedstrategy.cpp \
../../energyplugin/schedulingstrategies/aistrategy.cpp \
../../energyplugin/schedulingstrategies/manualstrategy.cpp \
../../energyplugin/types/chargingaction.cpp \
../../energyplugin/types/charginginfo.cpp \
../../energyplugin/types/chargingprocessinfo.cpp \
../../energyplugin/types/chargingschedule.cpp \
../../energyplugin/types/energytimeslot.cpp \
../../energyplugin/types/flexibleload.cpp \
../../energyplugin/types/manualslotconfig.cpp \
../../energyplugin/types/schedulerconfig.cpp \
../../energyplugin/types/scoreentry.cpp \
../../energyplugin/types/smartchargingstate.cpp \
@ -608,8 +618,8 @@ distdir: FORCE
@test -d $(DISTDIR) || mkdir -p $(DISTDIR)
$(COPY_FILE) --parents $(DIST) $(DISTDIR)/
$(COPY_FILE) --parents /usr/lib/x86_64-linux-gnu/qt6/mkspecs/features/data/dummy.cpp $(DISTDIR)/
$(COPY_FILE) --parents ../../energyplugin/energymanagerconfiguration.h ../../energyplugin/energysettings.h ../../energyplugin/evcharger.h ../../energyplugin/nymeaenergyjsonhandler.h ../../energyplugin/rootmeter.h ../../energyplugin/schedulermanager.h ../../energyplugin/schedulersettings.h ../../energyplugin/smartchargingmanager.h ../../energyplugin/spotmarket/spotmarketdataprovider.h ../../energyplugin/spotmarket/spotmarketdataproviderawattar.h ../../energyplugin/spotmarket/spotmarketmanager.h ../../energyplugin/schedulingstrategies/ischedulingstrategy.h ../../energyplugin/schedulingstrategies/rulebasedstrategy.h ../../energyplugin/schedulingstrategies/aistrategy.h ../../energyplugin/types/chargingaction.h ../../energyplugin/types/charginginfo.h ../../energyplugin/types/chargingprocessinfo.h ../../energyplugin/types/chargingschedule.h ../../energyplugin/types/energytimeslot.h ../../energyplugin/types/flexibleload.h ../../energyplugin/types/schedulerconfig.h ../../energyplugin/types/scoreentry.h ../../energyplugin/types/smartchargingstate.h ../../energyplugin/types/timeframe.h ../../energyplugin/energypluginnymea.h $(DISTDIR)/
$(COPY_FILE) --parents ../../energyplugin/energymanagerconfiguration.cpp ../../energyplugin/energysettings.cpp ../../energyplugin/evcharger.cpp ../../energyplugin/nymeaenergyjsonhandler.cpp ../../energyplugin/rootmeter.cpp ../../energyplugin/schedulermanager.cpp ../../energyplugin/schedulersettings.cpp ../../energyplugin/smartchargingmanager.cpp ../../energyplugin/spotmarket/spotmarketdataprovider.cpp ../../energyplugin/spotmarket/spotmarketdataproviderawattar.cpp ../../energyplugin/spotmarket/spotmarketmanager.cpp ../../energyplugin/schedulingstrategies/rulebasedstrategy.cpp ../../energyplugin/schedulingstrategies/aistrategy.cpp ../../energyplugin/types/chargingaction.cpp ../../energyplugin/types/charginginfo.cpp ../../energyplugin/types/chargingprocessinfo.cpp ../../energyplugin/types/chargingschedule.cpp ../../energyplugin/types/energytimeslot.cpp ../../energyplugin/types/flexibleload.cpp ../../energyplugin/types/schedulerconfig.cpp ../../energyplugin/types/scoreentry.cpp ../../energyplugin/types/smartchargingstate.cpp ../../energyplugin/types/timeframe.cpp ../../energyplugin/energypluginnymea.cpp $(DISTDIR)/
$(COPY_FILE) --parents ../../energyplugin/energymanagerconfiguration.h ../../energyplugin/energysettings.h ../../energyplugin/evcharger.h ../../energyplugin/nymeaenergyjsonhandler.h ../../energyplugin/rootmeter.h ../../energyplugin/schedulermanager.h ../../energyplugin/schedulersettings.h ../../energyplugin/smartchargingmanager.h ../../energyplugin/spotmarket/spotmarketdataprovider.h ../../energyplugin/spotmarket/spotmarketdataproviderawattar.h ../../energyplugin/spotmarket/spotmarketmanager.h ../../energyplugin/schedulingstrategies/ischedulingstrategy.h ../../energyplugin/schedulingstrategies/rulebasedstrategy.h ../../energyplugin/schedulingstrategies/aistrategy.h ../../energyplugin/schedulingstrategies/manualstrategy.h ../../energyplugin/types/chargingaction.h ../../energyplugin/types/charginginfo.h ../../energyplugin/types/chargingprocessinfo.h ../../energyplugin/types/chargingschedule.h ../../energyplugin/types/energytimeslot.h ../../energyplugin/types/flexibleload.h ../../energyplugin/types/manualslotconfig.h ../../energyplugin/types/schedulerconfig.h ../../energyplugin/types/scoreentry.h ../../energyplugin/types/smartchargingstate.h ../../energyplugin/types/timeframe.h ../../energyplugin/energypluginnymea.h $(DISTDIR)/
$(COPY_FILE) --parents ../../energyplugin/energymanagerconfiguration.cpp ../../energyplugin/energysettings.cpp ../../energyplugin/evcharger.cpp ../../energyplugin/nymeaenergyjsonhandler.cpp ../../energyplugin/rootmeter.cpp ../../energyplugin/schedulermanager.cpp ../../energyplugin/schedulersettings.cpp ../../energyplugin/smartchargingmanager.cpp ../../energyplugin/spotmarket/spotmarketdataprovider.cpp ../../energyplugin/spotmarket/spotmarketdataproviderawattar.cpp ../../energyplugin/spotmarket/spotmarketmanager.cpp ../../energyplugin/schedulingstrategies/rulebasedstrategy.cpp ../../energyplugin/schedulingstrategies/aistrategy.cpp ../../energyplugin/schedulingstrategies/manualstrategy.cpp ../../energyplugin/types/chargingaction.cpp ../../energyplugin/types/charginginfo.cpp ../../energyplugin/types/chargingprocessinfo.cpp ../../energyplugin/types/chargingschedule.cpp ../../energyplugin/types/energytimeslot.cpp ../../energyplugin/types/flexibleload.cpp ../../energyplugin/types/manualslotconfig.cpp ../../energyplugin/types/schedulerconfig.cpp ../../energyplugin/types/scoreentry.cpp ../../energyplugin/types/smartchargingstate.cpp ../../energyplugin/types/timeframe.cpp ../../energyplugin/energypluginnymea.cpp $(DISTDIR)/
$(COPY_FILE) --parents /home/etm/Projects/etm-nymea/nymea-energy-plugin-nymea/energyplugin/translations/nymea-energy-plugin-nymea-de.ts /home/etm/Projects/etm-nymea/nymea-energy-plugin-nymea/energyplugin/translations/nymea-energy-plugin-nymea-en_US.ts $(DISTDIR)/
@ -647,9 +657,9 @@ compiler_moc_predefs_clean:
moc_predefs.h: /usr/lib/x86_64-linux-gnu/qt6/mkspecs/features/data/dummy.cpp
g++ -pipe -std=c++17 -O2 -std=gnu++1z -Wall -Wextra -dM -E -o moc_predefs.h /usr/lib/x86_64-linux-gnu/qt6/mkspecs/features/data/dummy.cpp
compiler_moc_header_make_all: moc_energymanagerconfiguration.cpp moc_evcharger.cpp moc_nymeaenergyjsonhandler.cpp moc_rootmeter.cpp moc_schedulermanager.cpp moc_schedulersettings.cpp moc_smartchargingmanager.cpp moc_spotmarketdataprovider.cpp moc_spotmarketdataproviderawattar.cpp moc_spotmarketmanager.cpp moc_ischedulingstrategy.cpp moc_rulebasedstrategy.cpp moc_aistrategy.cpp moc_chargingaction.cpp moc_charginginfo.cpp moc_chargingschedule.cpp moc_scoreentry.cpp moc_smartchargingstate.cpp moc_energypluginnymea.cpp
compiler_moc_header_make_all: moc_energymanagerconfiguration.cpp moc_evcharger.cpp moc_nymeaenergyjsonhandler.cpp moc_rootmeter.cpp moc_schedulermanager.cpp moc_schedulersettings.cpp moc_smartchargingmanager.cpp moc_spotmarketdataprovider.cpp moc_spotmarketdataproviderawattar.cpp moc_spotmarketmanager.cpp moc_ischedulingstrategy.cpp moc_rulebasedstrategy.cpp moc_aistrategy.cpp moc_manualstrategy.cpp moc_chargingaction.cpp moc_charginginfo.cpp moc_chargingschedule.cpp moc_scoreentry.cpp moc_smartchargingstate.cpp moc_energypluginnymea.cpp
compiler_moc_header_clean:
-$(DEL_FILE) moc_energymanagerconfiguration.cpp moc_evcharger.cpp moc_nymeaenergyjsonhandler.cpp moc_rootmeter.cpp moc_schedulermanager.cpp moc_schedulersettings.cpp moc_smartchargingmanager.cpp moc_spotmarketdataprovider.cpp moc_spotmarketdataproviderawattar.cpp moc_spotmarketmanager.cpp moc_ischedulingstrategy.cpp moc_rulebasedstrategy.cpp moc_aistrategy.cpp moc_chargingaction.cpp moc_charginginfo.cpp moc_chargingschedule.cpp moc_scoreentry.cpp moc_smartchargingstate.cpp moc_energypluginnymea.cpp
-$(DEL_FILE) moc_energymanagerconfiguration.cpp moc_evcharger.cpp moc_nymeaenergyjsonhandler.cpp moc_rootmeter.cpp moc_schedulermanager.cpp moc_schedulersettings.cpp moc_smartchargingmanager.cpp moc_spotmarketdataprovider.cpp moc_spotmarketdataproviderawattar.cpp moc_spotmarketmanager.cpp moc_ischedulingstrategy.cpp moc_rulebasedstrategy.cpp moc_aistrategy.cpp moc_manualstrategy.cpp moc_chargingaction.cpp moc_charginginfo.cpp moc_chargingschedule.cpp moc_scoreentry.cpp moc_smartchargingstate.cpp moc_energypluginnymea.cpp
moc_energymanagerconfiguration.cpp: ../../energyplugin/energymanagerconfiguration.h \
moc_predefs.h \
/usr/lib/qt6/libexec/moc
@ -674,6 +684,8 @@ moc_nymeaenergyjsonhandler.cpp: ../../energyplugin/nymeaenergyjsonhandler.h \
../../energyplugin/types/scoreentry.h \
../../energyplugin/types/timeframe.h \
../../energyplugin/types/energytimeslot.h \
../../energyplugin/types/manualslotconfig.h \
../../energyplugin/types/flexibleload.h \
moc_predefs.h \
/usr/lib/qt6/libexec/moc
/usr/lib/qt6/libexec/moc $(DEFINES) --include /home/etm/Projects/etm-nymea/nymea-energy-plugin-nymea/build-test/energyplugin/moc_predefs.h -I/usr/lib/x86_64-linux-gnu/qt6/mkspecs/linux-g++ -I/home/etm/Projects/etm-nymea/nymea-energy-plugin-nymea/energyplugin -I/usr/include/nymea -I/usr/include/nymea-energy -I/usr/include/x86_64-linux-gnu/qt6 -I/usr/include/x86_64-linux-gnu/qt6/QtGui -I/usr/include/x86_64-linux-gnu/qt6/QtNetwork -I/usr/include/x86_64-linux-gnu/qt6/QtCore -I. -I/usr/include/c++/14 -I/usr/include/x86_64-linux-gnu/c++/14 -I/usr/include/c++/14/backward -I/usr/lib/gcc/x86_64-linux-gnu/14/include -I/usr/local/include -I/usr/include/x86_64-linux-gnu -I/usr/include ../../energyplugin/nymeaenergyjsonhandler.h -o moc_nymeaenergyjsonhandler.cpp
@ -697,6 +709,7 @@ moc_schedulermanager.cpp: ../../energyplugin/schedulermanager.h \
../../energyplugin/types/energytimeslot.h \
../../energyplugin/types/flexibleload.h \
../../energyplugin/types/schedulerconfig.h \
../../energyplugin/types/manualslotconfig.h \
../../energyplugin/schedulingstrategies/ischedulingstrategy.h \
moc_predefs.h \
/usr/lib/qt6/libexec/moc
@ -706,6 +719,7 @@ moc_schedulersettings.cpp: ../../energyplugin/schedulersettings.h \
../../energyplugin/types/schedulerconfig.h \
../../energyplugin/types/flexibleload.h \
../../energyplugin/types/energytimeslot.h \
../../energyplugin/types/manualslotconfig.h \
moc_predefs.h \
/usr/lib/qt6/libexec/moc
/usr/lib/qt6/libexec/moc $(DEFINES) --include /home/etm/Projects/etm-nymea/nymea-energy-plugin-nymea/build-test/energyplugin/moc_predefs.h -I/usr/lib/x86_64-linux-gnu/qt6/mkspecs/linux-g++ -I/home/etm/Projects/etm-nymea/nymea-energy-plugin-nymea/energyplugin -I/usr/include/nymea -I/usr/include/nymea-energy -I/usr/include/x86_64-linux-gnu/qt6 -I/usr/include/x86_64-linux-gnu/qt6/QtGui -I/usr/include/x86_64-linux-gnu/qt6/QtNetwork -I/usr/include/x86_64-linux-gnu/qt6/QtCore -I. -I/usr/include/c++/14 -I/usr/include/x86_64-linux-gnu/c++/14 -I/usr/include/c++/14/backward -I/usr/lib/gcc/x86_64-linux-gnu/14/include -I/usr/local/include -I/usr/include/x86_64-linux-gnu -I/usr/include ../../energyplugin/schedulersettings.h -o moc_schedulersettings.cpp
@ -775,6 +789,16 @@ moc_aistrategy.cpp: ../../energyplugin/schedulingstrategies/aistrategy.h \
/usr/lib/qt6/libexec/moc
/usr/lib/qt6/libexec/moc $(DEFINES) --include /home/etm/Projects/etm-nymea/nymea-energy-plugin-nymea/build-test/energyplugin/moc_predefs.h -I/usr/lib/x86_64-linux-gnu/qt6/mkspecs/linux-g++ -I/home/etm/Projects/etm-nymea/nymea-energy-plugin-nymea/energyplugin -I/usr/include/nymea -I/usr/include/nymea-energy -I/usr/include/x86_64-linux-gnu/qt6 -I/usr/include/x86_64-linux-gnu/qt6/QtGui -I/usr/include/x86_64-linux-gnu/qt6/QtNetwork -I/usr/include/x86_64-linux-gnu/qt6/QtCore -I. -I/usr/include/c++/14 -I/usr/include/x86_64-linux-gnu/c++/14 -I/usr/include/c++/14/backward -I/usr/lib/gcc/x86_64-linux-gnu/14/include -I/usr/local/include -I/usr/include/x86_64-linux-gnu -I/usr/include ../../energyplugin/schedulingstrategies/aistrategy.h -o moc_aistrategy.cpp
moc_manualstrategy.cpp: ../../energyplugin/schedulingstrategies/manualstrategy.h \
../../energyplugin/schedulingstrategies/ischedulingstrategy.h \
../../energyplugin/types/energytimeslot.h \
../../energyplugin/types/flexibleload.h \
../../energyplugin/types/schedulerconfig.h \
../../energyplugin/types/manualslotconfig.h \
moc_predefs.h \
/usr/lib/qt6/libexec/moc
/usr/lib/qt6/libexec/moc $(DEFINES) --include /home/etm/Projects/etm-nymea/nymea-energy-plugin-nymea/build-test/energyplugin/moc_predefs.h -I/usr/lib/x86_64-linux-gnu/qt6/mkspecs/linux-g++ -I/home/etm/Projects/etm-nymea/nymea-energy-plugin-nymea/energyplugin -I/usr/include/nymea -I/usr/include/nymea-energy -I/usr/include/x86_64-linux-gnu/qt6 -I/usr/include/x86_64-linux-gnu/qt6/QtGui -I/usr/include/x86_64-linux-gnu/qt6/QtNetwork -I/usr/include/x86_64-linux-gnu/qt6/QtCore -I. -I/usr/include/c++/14 -I/usr/include/x86_64-linux-gnu/c++/14 -I/usr/include/c++/14/backward -I/usr/lib/gcc/x86_64-linux-gnu/14/include -I/usr/local/include -I/usr/include/x86_64-linux-gnu -I/usr/include ../../energyplugin/schedulingstrategies/manualstrategy.h -o moc_manualstrategy.cpp
moc_chargingaction.cpp: ../../energyplugin/types/chargingaction.h \
moc_predefs.h \
/usr/lib/qt6/libexec/moc
@ -845,6 +869,8 @@ nymeaenergyjsonhandler.o: ../../energyplugin/nymeaenergyjsonhandler.cpp ../../en
../../energyplugin/types/scoreentry.h \
../../energyplugin/types/timeframe.h \
../../energyplugin/types/energytimeslot.h \
../../energyplugin/types/manualslotconfig.h \
../../energyplugin/types/flexibleload.h \
../../energyplugin/types/charginginfo.h \
../../energyplugin/smartchargingmanager.h \
../../energyplugin/energymanagerconfiguration.h \
@ -854,9 +880,9 @@ nymeaenergyjsonhandler.o: ../../energyplugin/nymeaenergyjsonhandler.cpp ../../en
../../energyplugin/spotmarket/spotmarketmanager.h \
../../energyplugin/spotmarket/spotmarketdataprovider.h \
../../energyplugin/schedulermanager.h \
../../energyplugin/types/flexibleload.h \
../../energyplugin/types/schedulerconfig.h \
../../energyplugin/schedulingstrategies/ischedulingstrategy.h
../../energyplugin/schedulingstrategies/ischedulingstrategy.h \
../../energyplugin/schedulingstrategies/manualstrategy.h
$(CXX) -c $(CXXFLAGS) $(INCPATH) -o nymeaenergyjsonhandler.o ../../energyplugin/nymeaenergyjsonhandler.cpp
rootmeter.o: ../../energyplugin/rootmeter.cpp ../../energyplugin/rootmeter.h \
@ -876,9 +902,12 @@ schedulermanager.o: ../../energyplugin/schedulermanager.cpp ../../energyplugin/s
../../energyplugin/types/energytimeslot.h \
../../energyplugin/types/flexibleload.h \
../../energyplugin/types/schedulerconfig.h \
../../energyplugin/types/manualslotconfig.h \
../../energyplugin/schedulingstrategies/ischedulingstrategy.h \
../../energyplugin/schedulingstrategies/rulebasedstrategy.h \
../../energyplugin/schedulingstrategies/aistrategy.h \
../../energyplugin/schedulingstrategies/manualstrategy.h \
../../energyplugin/schedulersettings.h \
../../energyplugin/spotmarket/spotmarketmanager.h \
../../energyplugin/types/chargingschedule.h \
../../energyplugin/types/timeframe.h \
@ -890,7 +919,8 @@ schedulermanager.o: ../../energyplugin/schedulermanager.cpp ../../energyplugin/s
schedulersettings.o: ../../energyplugin/schedulersettings.cpp ../../energyplugin/schedulersettings.h \
../../energyplugin/types/schedulerconfig.h \
../../energyplugin/types/flexibleload.h \
../../energyplugin/types/energytimeslot.h
../../energyplugin/types/energytimeslot.h \
../../energyplugin/types/manualslotconfig.h
$(CXX) -c $(CXXFLAGS) $(INCPATH) -o schedulersettings.o ../../energyplugin/schedulersettings.cpp
smartchargingmanager.o: ../../energyplugin/smartchargingmanager.cpp ../../energyplugin/smartchargingmanager.h \
@ -946,6 +976,14 @@ aistrategy.o: ../../energyplugin/schedulingstrategies/aistrategy.cpp ../../energ
../../energyplugin/types/schedulerconfig.h
$(CXX) -c $(CXXFLAGS) $(INCPATH) -o aistrategy.o ../../energyplugin/schedulingstrategies/aistrategy.cpp
manualstrategy.o: ../../energyplugin/schedulingstrategies/manualstrategy.cpp ../../energyplugin/schedulingstrategies/manualstrategy.h \
../../energyplugin/schedulingstrategies/ischedulingstrategy.h \
../../energyplugin/types/energytimeslot.h \
../../energyplugin/types/flexibleload.h \
../../energyplugin/types/schedulerconfig.h \
../../energyplugin/types/manualslotconfig.h
$(CXX) -c $(CXXFLAGS) $(INCPATH) -o manualstrategy.o ../../energyplugin/schedulingstrategies/manualstrategy.cpp
chargingaction.o: ../../energyplugin/types/chargingaction.cpp ../../energyplugin/types/chargingaction.h
$(CXX) -c $(CXXFLAGS) $(INCPATH) -o chargingaction.o ../../energyplugin/types/chargingaction.cpp
@ -969,6 +1007,10 @@ energytimeslot.o: ../../energyplugin/types/energytimeslot.cpp ../../energyplugin
flexibleload.o: ../../energyplugin/types/flexibleload.cpp ../../energyplugin/types/flexibleload.h
$(CXX) -c $(CXXFLAGS) $(INCPATH) -o flexibleload.o ../../energyplugin/types/flexibleload.cpp
manualslotconfig.o: ../../energyplugin/types/manualslotconfig.cpp ../../energyplugin/types/manualslotconfig.h \
../../energyplugin/types/flexibleload.h
$(CXX) -c $(CXXFLAGS) $(INCPATH) -o manualslotconfig.o ../../energyplugin/types/manualslotconfig.cpp
schedulerconfig.o: ../../energyplugin/types/schedulerconfig.cpp ../../energyplugin/types/schedulerconfig.h
$(CXX) -c $(CXXFLAGS) $(INCPATH) -o schedulerconfig.o ../../energyplugin/types/schedulerconfig.cpp
@ -997,8 +1039,10 @@ energypluginnymea.o: ../../energyplugin/energypluginnymea.cpp ../../energyplugin
../../energyplugin/types/energytimeslot.h \
../../energyplugin/types/flexibleload.h \
../../energyplugin/types/schedulerconfig.h \
../../energyplugin/types/manualslotconfig.h \
../../energyplugin/schedulingstrategies/ischedulingstrategy.h \
../../energyplugin/nymeaenergyjsonhandler.h \
../../energyplugin/schedulingstrategies/manualstrategy.h \
../../energyplugin/plugininfo.h
$(CXX) -c $(CXXFLAGS) $(INCPATH) -o energypluginnymea.o ../../energyplugin/energypluginnymea.cpp
@ -1041,6 +1085,9 @@ moc_rulebasedstrategy.o: moc_rulebasedstrategy.cpp
moc_aistrategy.o: moc_aistrategy.cpp
$(CXX) -c $(CXXFLAGS) $(INCPATH) -o moc_aistrategy.o moc_aistrategy.cpp
moc_manualstrategy.o: moc_manualstrategy.cpp
$(CXX) -c $(CXXFLAGS) $(INCPATH) -o moc_manualstrategy.o moc_manualstrategy.cpp
moc_chargingaction.o: moc_chargingaction.cpp
$(CXX) -c $(CXXFLAGS) $(INCPATH) -o moc_chargingaction.o moc_chargingaction.cpp

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,102 @@
/****************************************************************************
** Meta object code from reading C++ file 'manualstrategy.h'
**
** Created by: The Qt Meta Object Compiler version 68 (Qt 6.8.2)
**
** WARNING! All changes made in this file will be lost!
*****************************************************************************/
#include "../../energyplugin/schedulingstrategies/manualstrategy.h"
#include <QtCore/qmetatype.h>
#include <QtCore/qtmochelpers.h>
#include <memory>
#include <QtCore/qxptype_traits.h>
#if !defined(Q_MOC_OUTPUT_REVISION)
#error "The header file 'manualstrategy.h' doesn't include <QObject>."
#elif Q_MOC_OUTPUT_REVISION != 68
#error "This file was generated using the moc from 6.8.2. It"
#error "cannot be used with the include files from this version of Qt."
#error "(The moc has changed too much.)"
#endif
#ifndef Q_CONSTINIT
#define Q_CONSTINIT
#endif
QT_WARNING_PUSH
QT_WARNING_DISABLE_DEPRECATED
QT_WARNING_DISABLE_GCC("-Wuseless-cast")
namespace {
struct qt_meta_tag_ZN14ManualStrategyE_t {};
} // unnamed namespace
#ifdef QT_MOC_HAS_STRINGDATA
static constexpr auto qt_meta_stringdata_ZN14ManualStrategyE = QtMocHelpers::stringData(
"ManualStrategy"
);
#else // !QT_MOC_HAS_STRINGDATA
#error "qtmochelpers.h not found or too old."
#endif // !QT_MOC_HAS_STRINGDATA
Q_CONSTINIT static const uint qt_meta_data_ZN14ManualStrategyE[] = {
// content:
12, // revision
0, // classname
0, 0, // classinfo
0, 0, // methods
0, 0, // properties
0, 0, // enums/sets
0, 0, // constructors
0, // flags
0, // signalCount
0 // eod
};
Q_CONSTINIT const QMetaObject ManualStrategy::staticMetaObject = { {
QMetaObject::SuperData::link<ISchedulingStrategy::staticMetaObject>(),
qt_meta_stringdata_ZN14ManualStrategyE.offsetsAndSizes,
qt_meta_data_ZN14ManualStrategyE,
qt_static_metacall,
nullptr,
qt_incomplete_metaTypeArray<qt_meta_tag_ZN14ManualStrategyE_t,
// Q_OBJECT / Q_GADGET
QtPrivate::TypeAndForceComplete<ManualStrategy, std::true_type>
>,
nullptr
} };
void ManualStrategy::qt_static_metacall(QObject *_o, QMetaObject::Call _c, int _id, void **_a)
{
auto *_t = static_cast<ManualStrategy *>(_o);
(void)_t;
(void)_c;
(void)_id;
(void)_a;
}
const QMetaObject *ManualStrategy::metaObject() const
{
return QObject::d_ptr->metaObject ? QObject::d_ptr->dynamicMetaObject() : &staticMetaObject;
}
void *ManualStrategy::qt_metacast(const char *_clname)
{
if (!_clname) return nullptr;
if (!strcmp(_clname, qt_meta_stringdata_ZN14ManualStrategyE.stringdata0))
return static_cast<void*>(this);
return ISchedulingStrategy::qt_metacast(_clname);
}
int ManualStrategy::qt_metacall(QMetaObject::Call _c, int _id, void **_a)
{
_id = ISchedulingStrategy::qt_metacall(_c, _id, _a);
return _id;
}
QT_WARNING_POP

Binary file not shown.

View File

@ -54,6 +54,7 @@ static constexpr auto qt_meta_stringdata_ZN22NymeaEnergyJsonHandlerE = QtMocHelp
"TimelineUpdated",
"SlotActivated",
"OverrideConflict",
"ManualSlotActivated",
"GetPhasePowerLimit",
"JsonReply*",
"SetPhasePowerLimit",
@ -78,7 +79,11 @@ static constexpr auto qt_meta_stringdata_ZN22NymeaEnergyJsonHandlerE = QtMocHelp
"SetSchedulerStrategy",
"SetSchedulerConfig",
"SetLoadConfig",
"OverrideSlot"
"OverrideSlot",
"GetManualSlots",
"SetManualSlot",
"RemoveManualSlot",
"ClearManualSlots"
);
#else // !QT_MOC_HAS_STRINGDATA
#error "qtmochelpers.h not found or too old."
@ -90,51 +95,56 @@ Q_CONSTINIT static const uint qt_meta_data_ZN22NymeaEnergyJsonHandlerE[] = {
12, // revision
0, // classname
0, 0, // classinfo
35, 14, // methods
40, 14, // methods
0, 0, // properties
0, 0, // enums/sets
0, 0, // constructors
0, // flags
13, // signalCount
14, // signalCount
// signals: name, argc, parameters, tag, flags, initial metatype offsets
1, 1, 224, 2, 0x06, 1 /* Public */,
5, 1, 227, 2, 0x06, 3 /* Public */,
6, 1, 230, 2, 0x06, 5 /* Public */,
7, 1, 233, 2, 0x06, 7 /* Public */,
8, 1, 236, 2, 0x06, 9 /* Public */,
9, 1, 239, 2, 0x06, 11 /* Public */,
10, 1, 242, 2, 0x06, 13 /* Public */,
11, 1, 245, 2, 0x06, 15 /* Public */,
12, 1, 248, 2, 0x06, 17 /* Public */,
13, 1, 251, 2, 0x06, 19 /* Public */,
14, 1, 254, 2, 0x06, 21 /* Public */,
15, 1, 257, 2, 0x06, 23 /* Public */,
16, 1, 260, 2, 0x06, 25 /* Public */,
1, 1, 254, 2, 0x06, 1 /* Public */,
5, 1, 257, 2, 0x06, 3 /* Public */,
6, 1, 260, 2, 0x06, 5 /* Public */,
7, 1, 263, 2, 0x06, 7 /* Public */,
8, 1, 266, 2, 0x06, 9 /* Public */,
9, 1, 269, 2, 0x06, 11 /* Public */,
10, 1, 272, 2, 0x06, 13 /* Public */,
11, 1, 275, 2, 0x06, 15 /* Public */,
12, 1, 278, 2, 0x06, 17 /* Public */,
13, 1, 281, 2, 0x06, 19 /* Public */,
14, 1, 284, 2, 0x06, 21 /* Public */,
15, 1, 287, 2, 0x06, 23 /* Public */,
16, 1, 290, 2, 0x06, 25 /* Public */,
17, 1, 293, 2, 0x06, 27 /* Public */,
// methods: name, argc, parameters, tag, flags, initial metatype offsets
17, 1, 263, 2, 0x02, 27 /* Public */,
19, 1, 266, 2, 0x02, 29 /* Public */,
20, 1, 269, 2, 0x02, 31 /* Public */,
21, 1, 272, 2, 0x02, 33 /* Public */,
22, 1, 275, 2, 0x02, 35 /* Public */,
23, 1, 278, 2, 0x02, 37 /* Public */,
24, 1, 281, 2, 0x02, 39 /* Public */,
25, 2, 284, 2, 0x02, 41 /* Public */,
28, 1, 289, 2, 0x02, 44 /* Public */,
29, 1, 292, 2, 0x02, 46 /* Public */,
30, 1, 295, 2, 0x02, 48 /* Public */,
31, 1, 298, 2, 0x02, 50 /* Public */,
32, 1, 301, 2, 0x02, 52 /* Public */,
33, 1, 304, 2, 0x02, 54 /* Public */,
34, 1, 307, 2, 0x02, 56 /* Public */,
35, 1, 310, 2, 0x02, 58 /* Public */,
36, 1, 313, 2, 0x02, 60 /* Public */,
37, 1, 316, 2, 0x02, 62 /* Public */,
38, 1, 319, 2, 0x02, 64 /* Public */,
39, 1, 322, 2, 0x02, 66 /* Public */,
40, 1, 325, 2, 0x02, 68 /* Public */,
41, 1, 328, 2, 0x02, 70 /* Public */,
18, 1, 296, 2, 0x02, 29 /* Public */,
20, 1, 299, 2, 0x02, 31 /* Public */,
21, 1, 302, 2, 0x02, 33 /* Public */,
22, 1, 305, 2, 0x02, 35 /* Public */,
23, 1, 308, 2, 0x02, 37 /* Public */,
24, 1, 311, 2, 0x02, 39 /* Public */,
25, 1, 314, 2, 0x02, 41 /* Public */,
26, 2, 317, 2, 0x02, 43 /* Public */,
29, 1, 322, 2, 0x02, 46 /* Public */,
30, 1, 325, 2, 0x02, 48 /* Public */,
31, 1, 328, 2, 0x02, 50 /* Public */,
32, 1, 331, 2, 0x02, 52 /* Public */,
33, 1, 334, 2, 0x02, 54 /* Public */,
34, 1, 337, 2, 0x02, 56 /* Public */,
35, 1, 340, 2, 0x02, 58 /* Public */,
36, 1, 343, 2, 0x02, 60 /* Public */,
37, 1, 346, 2, 0x02, 62 /* Public */,
38, 1, 349, 2, 0x02, 64 /* Public */,
39, 1, 352, 2, 0x02, 66 /* Public */,
40, 1, 355, 2, 0x02, 68 /* Public */,
41, 1, 358, 2, 0x02, 70 /* Public */,
42, 1, 361, 2, 0x02, 72 /* Public */,
43, 1, 364, 2, 0x02, 74 /* Public */,
44, 1, 367, 2, 0x02, 76 /* Public */,
45, 1, 370, 2, 0x02, 78 /* Public */,
46, 1, 373, 2, 0x02, 80 /* Public */,
// signals: parameters
QMetaType::Void, 0x80000000 | 3, 4,
@ -150,30 +160,35 @@ Q_CONSTINIT static const uint qt_meta_data_ZN22NymeaEnergyJsonHandlerE[] = {
QMetaType::Void, 0x80000000 | 3, 4,
QMetaType::Void, 0x80000000 | 3, 4,
QMetaType::Void, 0x80000000 | 3, 4,
QMetaType::Void, 0x80000000 | 3, 4,
// methods: parameters
0x80000000 | 18, 0x80000000 | 3, 4,
0x80000000 | 18, 0x80000000 | 3, 4,
0x80000000 | 18, 0x80000000 | 3, 4,
0x80000000 | 18, 0x80000000 | 3, 4,
0x80000000 | 18, 0x80000000 | 3, 4,
0x80000000 | 18, 0x80000000 | 3, 4,
0x80000000 | 18, 0x80000000 | 3, 4,
0x80000000 | 18, 0x80000000 | 3, 0x80000000 | 26, 4, 27,
0x80000000 | 18, 0x80000000 | 3, 4,
0x80000000 | 18, 0x80000000 | 3, 4,
0x80000000 | 18, 0x80000000 | 3, 4,
0x80000000 | 18, 0x80000000 | 3, 4,
0x80000000 | 18, 0x80000000 | 3, 4,
0x80000000 | 18, 0x80000000 | 3, 4,
0x80000000 | 18, 0x80000000 | 3, 4,
0x80000000 | 18, 0x80000000 | 3, 4,
0x80000000 | 18, 0x80000000 | 3, 4,
0x80000000 | 18, 0x80000000 | 3, 4,
0x80000000 | 18, 0x80000000 | 3, 4,
0x80000000 | 18, 0x80000000 | 3, 4,
0x80000000 | 18, 0x80000000 | 3, 4,
0x80000000 | 18, 0x80000000 | 3, 4,
0x80000000 | 19, 0x80000000 | 3, 4,
0x80000000 | 19, 0x80000000 | 3, 4,
0x80000000 | 19, 0x80000000 | 3, 4,
0x80000000 | 19, 0x80000000 | 3, 4,
0x80000000 | 19, 0x80000000 | 3, 4,
0x80000000 | 19, 0x80000000 | 3, 4,
0x80000000 | 19, 0x80000000 | 3, 4,
0x80000000 | 19, 0x80000000 | 3, 0x80000000 | 27, 4, 28,
0x80000000 | 19, 0x80000000 | 3, 4,
0x80000000 | 19, 0x80000000 | 3, 4,
0x80000000 | 19, 0x80000000 | 3, 4,
0x80000000 | 19, 0x80000000 | 3, 4,
0x80000000 | 19, 0x80000000 | 3, 4,
0x80000000 | 19, 0x80000000 | 3, 4,
0x80000000 | 19, 0x80000000 | 3, 4,
0x80000000 | 19, 0x80000000 | 3, 4,
0x80000000 | 19, 0x80000000 | 3, 4,
0x80000000 | 19, 0x80000000 | 3, 4,
0x80000000 | 19, 0x80000000 | 3, 4,
0x80000000 | 19, 0x80000000 | 3, 4,
0x80000000 | 19, 0x80000000 | 3, 4,
0x80000000 | 19, 0x80000000 | 3, 4,
0x80000000 | 19, 0x80000000 | 3, 4,
0x80000000 | 19, 0x80000000 | 3, 4,
0x80000000 | 19, 0x80000000 | 3, 4,
0x80000000 | 19, 0x80000000 | 3, 4,
0 // eod
};
@ -226,6 +241,9 @@ Q_CONSTINIT const QMetaObject NymeaEnergyJsonHandler::staticMetaObject = { {
// method 'OverrideConflict'
QtPrivate::TypeAndForceComplete<void, std::false_type>,
QtPrivate::TypeAndForceComplete<const QVariantMap &, std::false_type>,
// method 'ManualSlotActivated'
QtPrivate::TypeAndForceComplete<void, std::false_type>,
QtPrivate::TypeAndForceComplete<const QVariantMap &, std::false_type>,
// method 'GetPhasePowerLimit'
QtPrivate::TypeAndForceComplete<JsonReply *, std::false_type>,
QtPrivate::TypeAndForceComplete<const QVariantMap &, std::false_type>,
@ -292,6 +310,18 @@ Q_CONSTINIT const QMetaObject NymeaEnergyJsonHandler::staticMetaObject = { {
QtPrivate::TypeAndForceComplete<const QVariantMap &, std::false_type>,
// method 'OverrideSlot'
QtPrivate::TypeAndForceComplete<JsonReply *, std::false_type>,
QtPrivate::TypeAndForceComplete<const QVariantMap &, std::false_type>,
// method 'GetManualSlots'
QtPrivate::TypeAndForceComplete<JsonReply *, std::false_type>,
QtPrivate::TypeAndForceComplete<const QVariantMap &, std::false_type>,
// method 'SetManualSlot'
QtPrivate::TypeAndForceComplete<JsonReply *, std::false_type>,
QtPrivate::TypeAndForceComplete<const QVariantMap &, std::false_type>,
// method 'RemoveManualSlot'
QtPrivate::TypeAndForceComplete<JsonReply *, std::false_type>,
QtPrivate::TypeAndForceComplete<const QVariantMap &, std::false_type>,
// method 'ClearManualSlots'
QtPrivate::TypeAndForceComplete<JsonReply *, std::false_type>,
QtPrivate::TypeAndForceComplete<const QVariantMap &, std::false_type>
>,
nullptr
@ -315,49 +345,58 @@ void NymeaEnergyJsonHandler::qt_static_metacall(QObject *_o, QMetaObject::Call _
case 10: _t->TimelineUpdated((*reinterpret_cast< std::add_pointer_t<QVariantMap>>(_a[1]))); break;
case 11: _t->SlotActivated((*reinterpret_cast< std::add_pointer_t<QVariantMap>>(_a[1]))); break;
case 12: _t->OverrideConflict((*reinterpret_cast< std::add_pointer_t<QVariantMap>>(_a[1]))); break;
case 13: { JsonReply* _r = _t->GetPhasePowerLimit((*reinterpret_cast< std::add_pointer_t<QVariantMap>>(_a[1])));
case 13: _t->ManualSlotActivated((*reinterpret_cast< std::add_pointer_t<QVariantMap>>(_a[1]))); break;
case 14: { JsonReply* _r = _t->GetPhasePowerLimit((*reinterpret_cast< std::add_pointer_t<QVariantMap>>(_a[1])));
if (_a[0]) *reinterpret_cast< JsonReply**>(_a[0]) = std::move(_r); } break;
case 14: { JsonReply* _r = _t->SetPhasePowerLimit((*reinterpret_cast< std::add_pointer_t<QVariantMap>>(_a[1])));
case 15: { JsonReply* _r = _t->SetPhasePowerLimit((*reinterpret_cast< std::add_pointer_t<QVariantMap>>(_a[1])));
if (_a[0]) *reinterpret_cast< JsonReply**>(_a[0]) = std::move(_r); } break;
case 15: { JsonReply* _r = _t->GetAcquisitionTolerance((*reinterpret_cast< std::add_pointer_t<QVariantMap>>(_a[1])));
case 16: { JsonReply* _r = _t->GetAcquisitionTolerance((*reinterpret_cast< std::add_pointer_t<QVariantMap>>(_a[1])));
if (_a[0]) *reinterpret_cast< JsonReply**>(_a[0]) = std::move(_r); } break;
case 16: { JsonReply* _r = _t->SetAcquisitionTolerance((*reinterpret_cast< std::add_pointer_t<QVariantMap>>(_a[1])));
case 17: { JsonReply* _r = _t->SetAcquisitionTolerance((*reinterpret_cast< std::add_pointer_t<QVariantMap>>(_a[1])));
if (_a[0]) *reinterpret_cast< JsonReply**>(_a[0]) = std::move(_r); } break;
case 17: { JsonReply* _r = _t->GetBatteryLevelConsideration((*reinterpret_cast< std::add_pointer_t<QVariantMap>>(_a[1])));
case 18: { JsonReply* _r = _t->GetBatteryLevelConsideration((*reinterpret_cast< std::add_pointer_t<QVariantMap>>(_a[1])));
if (_a[0]) *reinterpret_cast< JsonReply**>(_a[0]) = std::move(_r); } break;
case 18: { JsonReply* _r = _t->SetBatteryLevelConsideration((*reinterpret_cast< std::add_pointer_t<QVariantMap>>(_a[1])));
case 19: { JsonReply* _r = _t->SetBatteryLevelConsideration((*reinterpret_cast< std::add_pointer_t<QVariantMap>>(_a[1])));
if (_a[0]) *reinterpret_cast< JsonReply**>(_a[0]) = std::move(_r); } break;
case 19: { JsonReply* _r = _t->GetChargingInfos((*reinterpret_cast< std::add_pointer_t<QVariantMap>>(_a[1])));
case 20: { JsonReply* _r = _t->GetChargingInfos((*reinterpret_cast< std::add_pointer_t<QVariantMap>>(_a[1])));
if (_a[0]) *reinterpret_cast< JsonReply**>(_a[0]) = std::move(_r); } break;
case 20: { JsonReply* _r = _t->SetChargingInfo((*reinterpret_cast< std::add_pointer_t<QVariantMap>>(_a[1])),(*reinterpret_cast< std::add_pointer_t<JsonContext>>(_a[2])));
case 21: { JsonReply* _r = _t->SetChargingInfo((*reinterpret_cast< std::add_pointer_t<QVariantMap>>(_a[1])),(*reinterpret_cast< std::add_pointer_t<JsonContext>>(_a[2])));
if (_a[0]) *reinterpret_cast< JsonReply**>(_a[0]) = std::move(_r); } break;
case 21: { JsonReply* _r = _t->GetLockOnUnplug((*reinterpret_cast< std::add_pointer_t<QVariantMap>>(_a[1])));
case 22: { JsonReply* _r = _t->GetLockOnUnplug((*reinterpret_cast< std::add_pointer_t<QVariantMap>>(_a[1])));
if (_a[0]) *reinterpret_cast< JsonReply**>(_a[0]) = std::move(_r); } break;
case 22: { JsonReply* _r = _t->SetLockOnUnplug((*reinterpret_cast< std::add_pointer_t<QVariantMap>>(_a[1])));
case 23: { JsonReply* _r = _t->SetLockOnUnplug((*reinterpret_cast< std::add_pointer_t<QVariantMap>>(_a[1])));
if (_a[0]) *reinterpret_cast< JsonReply**>(_a[0]) = std::move(_r); } break;
case 23: { JsonReply* _r = _t->GetAvailableSpotMarketProviders((*reinterpret_cast< std::add_pointer_t<QVariantMap>>(_a[1])));
case 24: { JsonReply* _r = _t->GetAvailableSpotMarketProviders((*reinterpret_cast< std::add_pointer_t<QVariantMap>>(_a[1])));
if (_a[0]) *reinterpret_cast< JsonReply**>(_a[0]) = std::move(_r); } break;
case 24: { JsonReply* _r = _t->GetSpotMarketConfiguration((*reinterpret_cast< std::add_pointer_t<QVariantMap>>(_a[1])));
case 25: { JsonReply* _r = _t->GetSpotMarketConfiguration((*reinterpret_cast< std::add_pointer_t<QVariantMap>>(_a[1])));
if (_a[0]) *reinterpret_cast< JsonReply**>(_a[0]) = std::move(_r); } break;
case 25: { JsonReply* _r = _t->SetSpotMarketConfiguration((*reinterpret_cast< std::add_pointer_t<QVariantMap>>(_a[1])));
case 26: { JsonReply* _r = _t->SetSpotMarketConfiguration((*reinterpret_cast< std::add_pointer_t<QVariantMap>>(_a[1])));
if (_a[0]) *reinterpret_cast< JsonReply**>(_a[0]) = std::move(_r); } break;
case 26: { JsonReply* _r = _t->GetSpotMarketScoreEntries((*reinterpret_cast< std::add_pointer_t<QVariantMap>>(_a[1])));
case 27: { JsonReply* _r = _t->GetSpotMarketScoreEntries((*reinterpret_cast< std::add_pointer_t<QVariantMap>>(_a[1])));
if (_a[0]) *reinterpret_cast< JsonReply**>(_a[0]) = std::move(_r); } break;
case 27: { JsonReply* _r = _t->GetChargingSchedules((*reinterpret_cast< std::add_pointer_t<QVariantMap>>(_a[1])));
case 28: { JsonReply* _r = _t->GetChargingSchedules((*reinterpret_cast< std::add_pointer_t<QVariantMap>>(_a[1])));
if (_a[0]) *reinterpret_cast< JsonReply**>(_a[0]) = std::move(_r); } break;
case 28: { JsonReply* _r = _t->GetEnergyTimeline((*reinterpret_cast< std::add_pointer_t<QVariantMap>>(_a[1])));
case 29: { JsonReply* _r = _t->GetEnergyTimeline((*reinterpret_cast< std::add_pointer_t<QVariantMap>>(_a[1])));
if (_a[0]) *reinterpret_cast< JsonReply**>(_a[0]) = std::move(_r); } break;
case 29: { JsonReply* _r = _t->GetFlexibleLoads((*reinterpret_cast< std::add_pointer_t<QVariantMap>>(_a[1])));
case 30: { JsonReply* _r = _t->GetFlexibleLoads((*reinterpret_cast< std::add_pointer_t<QVariantMap>>(_a[1])));
if (_a[0]) *reinterpret_cast< JsonReply**>(_a[0]) = std::move(_r); } break;
case 30: { JsonReply* _r = _t->GetSchedulerStatus((*reinterpret_cast< std::add_pointer_t<QVariantMap>>(_a[1])));
case 31: { JsonReply* _r = _t->GetSchedulerStatus((*reinterpret_cast< std::add_pointer_t<QVariantMap>>(_a[1])));
if (_a[0]) *reinterpret_cast< JsonReply**>(_a[0]) = std::move(_r); } break;
case 31: { JsonReply* _r = _t->SetSchedulerStrategy((*reinterpret_cast< std::add_pointer_t<QVariantMap>>(_a[1])));
case 32: { JsonReply* _r = _t->SetSchedulerStrategy((*reinterpret_cast< std::add_pointer_t<QVariantMap>>(_a[1])));
if (_a[0]) *reinterpret_cast< JsonReply**>(_a[0]) = std::move(_r); } break;
case 32: { JsonReply* _r = _t->SetSchedulerConfig((*reinterpret_cast< std::add_pointer_t<QVariantMap>>(_a[1])));
case 33: { JsonReply* _r = _t->SetSchedulerConfig((*reinterpret_cast< std::add_pointer_t<QVariantMap>>(_a[1])));
if (_a[0]) *reinterpret_cast< JsonReply**>(_a[0]) = std::move(_r); } break;
case 33: { JsonReply* _r = _t->SetLoadConfig((*reinterpret_cast< std::add_pointer_t<QVariantMap>>(_a[1])));
case 34: { JsonReply* _r = _t->SetLoadConfig((*reinterpret_cast< std::add_pointer_t<QVariantMap>>(_a[1])));
if (_a[0]) *reinterpret_cast< JsonReply**>(_a[0]) = std::move(_r); } break;
case 34: { JsonReply* _r = _t->OverrideSlot((*reinterpret_cast< std::add_pointer_t<QVariantMap>>(_a[1])));
case 35: { JsonReply* _r = _t->OverrideSlot((*reinterpret_cast< std::add_pointer_t<QVariantMap>>(_a[1])));
if (_a[0]) *reinterpret_cast< JsonReply**>(_a[0]) = std::move(_r); } break;
case 36: { JsonReply* _r = _t->GetManualSlots((*reinterpret_cast< std::add_pointer_t<QVariantMap>>(_a[1])));
if (_a[0]) *reinterpret_cast< JsonReply**>(_a[0]) = std::move(_r); } break;
case 37: { JsonReply* _r = _t->SetManualSlot((*reinterpret_cast< std::add_pointer_t<QVariantMap>>(_a[1])));
if (_a[0]) *reinterpret_cast< JsonReply**>(_a[0]) = std::move(_r); } break;
case 38: { JsonReply* _r = _t->RemoveManualSlot((*reinterpret_cast< std::add_pointer_t<QVariantMap>>(_a[1])));
if (_a[0]) *reinterpret_cast< JsonReply**>(_a[0]) = std::move(_r); } break;
case 39: { JsonReply* _r = _t->ClearManualSlots((*reinterpret_cast< std::add_pointer_t<QVariantMap>>(_a[1])));
if (_a[0]) *reinterpret_cast< JsonReply**>(_a[0]) = std::move(_r); } break;
default: ;
}
@ -455,6 +494,13 @@ void NymeaEnergyJsonHandler::qt_static_metacall(QObject *_o, QMetaObject::Call _
return;
}
}
{
using _q_method_type = void (NymeaEnergyJsonHandler::*)(const QVariantMap & );
if (_q_method_type _q_method = &NymeaEnergyJsonHandler::ManualSlotActivated; *reinterpret_cast<_q_method_type *>(_a[1]) == _q_method) {
*result = 13;
return;
}
}
}
}
@ -477,14 +523,14 @@ int NymeaEnergyJsonHandler::qt_metacall(QMetaObject::Call _c, int _id, void **_a
if (_id < 0)
return _id;
if (_c == QMetaObject::InvokeMetaMethod) {
if (_id < 35)
if (_id < 40)
qt_static_metacall(this, _c, _id, _a);
_id -= 35;
_id -= 40;
}
if (_c == QMetaObject::RegisterMethodArgumentMetaType) {
if (_id < 35)
if (_id < 40)
*reinterpret_cast<QMetaType *>(_a[0]) = QMetaType();
_id -= 35;
_id -= 40;
}
return _id;
}
@ -579,4 +625,11 @@ void NymeaEnergyJsonHandler::OverrideConflict(const QVariantMap & _t1)
void *_a[] = { nullptr, const_cast<void*>(reinterpret_cast<const void*>(std::addressof(_t1))) };
QMetaObject::activate(this, &staticMetaObject, 12, _a);
}
// SIGNAL 13
void NymeaEnergyJsonHandler::ManualSlotActivated(const QVariantMap & _t1)
{
void *_a[] = { nullptr, const_cast<void*>(reinterpret_cast<const void*>(std::addressof(_t1))) };
QMetaObject::activate(this, &staticMetaObject, 13, _a);
}
QT_WARNING_POP

View File

@ -56,6 +56,7 @@ static constexpr auto qt_meta_stringdata_ZN16SchedulerManagerE = QtMocHelpers::s
"configChanged",
"SchedulerConfig",
"config",
"manualSlotsChanged",
"onRecomputeTimer",
"onSlotExecutionTimer"
);
@ -69,24 +70,25 @@ Q_CONSTINIT static const uint qt_meta_data_ZN16SchedulerManagerE[] = {
12, // revision
0, // classname
0, 0, // classinfo
8, 14, // methods
9, 14, // methods
0, 0, // properties
0, 0, // enums/sets
0, 0, // constructors
0, // flags
6, // signalCount
7, // signalCount
// signals: name, argc, parameters, tag, flags, initial metatype offsets
1, 1, 62, 2, 0x06, 1 /* Public */,
5, 2, 65, 2, 0x06, 3 /* Public */,
9, 1, 70, 2, 0x06, 6 /* Public */,
11, 1, 73, 2, 0x06, 8 /* Public */,
14, 1, 76, 2, 0x06, 10 /* Public */,
15, 1, 79, 2, 0x06, 12 /* Public */,
1, 1, 68, 2, 0x06, 1 /* Public */,
5, 2, 71, 2, 0x06, 3 /* Public */,
9, 1, 76, 2, 0x06, 6 /* Public */,
11, 1, 79, 2, 0x06, 8 /* Public */,
14, 1, 82, 2, 0x06, 10 /* Public */,
15, 1, 85, 2, 0x06, 12 /* Public */,
18, 0, 88, 2, 0x06, 14 /* Public */,
// slots: name, argc, parameters, tag, flags, initial metatype offsets
18, 0, 82, 2, 0x08, 14 /* Private */,
19, 0, 83, 2, 0x08, 15 /* Private */,
19, 0, 89, 2, 0x08, 15 /* Private */,
20, 0, 90, 2, 0x08, 16 /* Private */,
// signals: parameters
QMetaType::Void, 0x80000000 | 3, 4,
@ -95,6 +97,7 @@ Q_CONSTINIT static const uint qt_meta_data_ZN16SchedulerManagerE[] = {
QMetaType::Void, 0x80000000 | 12, 13,
QMetaType::Void, 0x80000000 | 12, 13,
QMetaType::Void, 0x80000000 | 16, 17,
QMetaType::Void,
// slots: parameters
QMetaType::Void,
@ -131,6 +134,8 @@ Q_CONSTINIT const QMetaObject SchedulerManager::staticMetaObject = { {
// method 'configChanged'
QtPrivate::TypeAndForceComplete<void, std::false_type>,
QtPrivate::TypeAndForceComplete<const SchedulerConfig &, std::false_type>,
// method 'manualSlotsChanged'
QtPrivate::TypeAndForceComplete<void, std::false_type>,
// method 'onRecomputeTimer'
QtPrivate::TypeAndForceComplete<void, std::false_type>,
// method 'onSlotExecutionTimer'
@ -150,8 +155,9 @@ void SchedulerManager::qt_static_metacall(QObject *_o, QMetaObject::Call _c, int
case 3: _t->loadRegistered((*reinterpret_cast< std::add_pointer_t<FlexibleLoad>>(_a[1]))); break;
case 4: _t->loadUpdated((*reinterpret_cast< std::add_pointer_t<FlexibleLoad>>(_a[1]))); break;
case 5: _t->configChanged((*reinterpret_cast< std::add_pointer_t<SchedulerConfig>>(_a[1]))); break;
case 6: _t->onRecomputeTimer(); break;
case 7: _t->onSlotExecutionTimer(); break;
case 6: _t->manualSlotsChanged(); break;
case 7: _t->onRecomputeTimer(); break;
case 8: _t->onSlotExecutionTimer(); break;
default: ;
}
}
@ -232,6 +238,13 @@ void SchedulerManager::qt_static_metacall(QObject *_o, QMetaObject::Call _c, int
return;
}
}
{
using _q_method_type = void (SchedulerManager::*)();
if (_q_method_type _q_method = &SchedulerManager::manualSlotsChanged; *reinterpret_cast<_q_method_type *>(_a[1]) == _q_method) {
*result = 6;
return;
}
}
}
}
@ -254,14 +267,14 @@ int SchedulerManager::qt_metacall(QMetaObject::Call _c, int _id, void **_a)
if (_id < 0)
return _id;
if (_c == QMetaObject::InvokeMetaMethod) {
if (_id < 8)
if (_id < 9)
qt_static_metacall(this, _c, _id, _a);
_id -= 8;
_id -= 9;
}
if (_c == QMetaObject::RegisterMethodArgumentMetaType) {
if (_id < 8)
if (_id < 9)
qt_static_metacall(this, _c, _id, _a);
_id -= 8;
_id -= 9;
}
return _id;
}
@ -307,4 +320,10 @@ void SchedulerManager::configChanged(const SchedulerConfig & _t1)
void *_a[] = { nullptr, const_cast<void*>(reinterpret_cast<const void*>(std::addressof(_t1))) };
QMetaObject::activate(this, &staticMetaObject, 5, _a);
}
// SIGNAL 6
void SchedulerManager::manualSlotsChanged()
{
QMetaObject::activate(this, &staticMetaObject, 6, nullptr);
}
QT_WARNING_POP

147
doc.md
View File

@ -1377,3 +1377,150 @@ Toute décision **doit** avoir un `decisionReason` non vide. La méthode
---
*Section 10 ajoutée le 2026-02-23 — SchedulerManager Phase 1 (stubs prédiction)*
---
## 11. ManualStrategy — Community Tier
### 11.1 Vue d'ensemble
`ManualStrategy` (`strategyId = "manual"`) est la stratégie de niveau Community.
Elle donne à l'utilisateur un **contrôle total** : chaque créneau horaire est piloté
par une `ManualSlotConfig` explicitement définie. Aucune optimisation automatique.
Cas d'usage typique : utilisateur technique qui sait exactement quand et à quelle
puissance charger son VE, sans déléguer la décision à un algorithme.
### 11.2 Comportement par cas
| Situation | Résultat | decisionRules |
|---|---|---|
| Slot dans une config active | Allocations appliquées exactement | `["ManualSlot"]` |
| Slot sans config | Charges inflexibles/critiques uniquement | `["ManualDefault"]` ou `["CriticalHeating"]` |
| Config existante mais expirée | Charges critiques uniquement | `["ExpiredSlot"]` |
| Slot en override manuel | Préservé tel quel | `["ManualOverride"]` |
**Invariant** : `decisionReason` n'est jamais vide (contrat `ISchedulingStrategy`).
### 11.3 Type ManualSlotConfig
```cpp
struct ManualSlotConfig {
QDateTime start;
QDateTime end;
QMap<LoadSource, double> powerAllocations; // "ev"→2000W, "battery"→1000W, ...
QString label; // affiché dans l'UI, ex. "Recharge VE nuit"
bool repeating; // si true : récurrence hebdomadaire (même jour/heure)
QDateTime expiresAt; // optionnel — ignoré après cette date
};
```
Pour les slots **répétables** (`repeating=true`) : la récurrence est calculée en
*minutes-de-semaine* (jour_semaine × 1440 + heure × 60 + minute), ce qui gère
correctement les slots overnight (ex. Lun 22:00 → Mar 06:00).
### 11.4 JSON-RPC — NymeaEnergy v11
#### GetManualSlots
```json
→ {}
← { "slots": [ { ManualSlotConfig }, ... ] }
```
#### SetManualSlot
```json
→ {
"start": "2026-02-24T22:00:00.000Z",
"end": "2026-02-25T06:00:00.000Z",
"label": "Recharge VE nuit",
"repeating": false,
"expiresAt": "2026-03-01T00:00:00.000Z",
"allocations": { "ev": 2000, "battery": 1000, "heatpump": 0, "dhw": 0 }
}
← { "energyError": "EnergyErrorNoError" }
```
#### RemoveManualSlot
```json
→ { "start": "2026-02-24T22:00:00.000Z" }
← { "energyError": "EnergyErrorNoError" }
```
#### ClearManualSlots
```json
→ {}
← { "energyError": "EnergyErrorNoError" }
```
#### ManualSlotActivated (push notification)
```json
{
"slot": { /* ManualSlotConfig */ },
"appliedAllocations": { "ev": 2000, "battery": 1000, "heatpump": 0, "dhw": 0, "feedin": 0 },
"reason": "Créneau manuel 'Recharge VE nuit' activé"
}
```
### 11.5 Persistance
Les `ManualSlotConfig` sont persistées dans :
```
NymeaSettings::settingsPath() + "/scheduler.conf" [section: manualSlots]
```
- **Chargement** : au démarrage, dans `SchedulerManager::registerStrategy()` lorsque
`ManualStrategy` est enregistrée. Les slots expirés sont ignorés à la lecture.
- **Sauvegarde** : à chaque `SetManualSlot` / `RemoveManualSlot` / `ClearManualSlots`.
### 11.6 Guide d'intégration — créneau EV hebdomadaire
**Étape 1** — Activer ManualStrategy :
```json
{ "method": "NymeaEnergy.SetSchedulerStrategy", "params": { "strategyId": "manual" } }
```
**Étape 2** — Configurer un créneau VE chaque lundi nuit (22:00→06:00), 2 kW :
```json
{
"method": "NymeaEnergy.SetManualSlot",
"params": {
"start": "2026-02-23T22:00:00.000Z",
"end": "2026-02-24T06:00:00.000Z",
"label": "Recharge hebdo VE",
"repeating": true,
"allocations": { "ev": 2000 }
}
}
```
**Étape 3** — S'abonner à la notification pour confirmation :
```json
{ "method": "JSONRPC.SetNotificationStatus",
"params": { "namespaces": ["NymeaEnergy"] } }
// → ManualSlotActivated émis à chaque lundi 22:00
```
**Étape 4** — Retirer le créneau si besoin :
```json
{ "method": "NymeaEnergy.RemoveManualSlot",
"params": { "start": "2026-02-23T22:00:00.000Z" } }
```
### 11.7 Clés d'allocation (JSON)
| Clé JSON | LoadSource interne |
|---|---|
| `"ev"` | `LoadSource::SmartCharging` |
| `"battery"` | `LoadSource::Battery` |
| `"dhw"` | `LoadSource::DHW` |
| `"heatpump"` | `LoadSource::HeatPump` |
| `"feedin"` | `LoadSource::FeedIn` |
---
*Section 11 ajoutée le 2026-02-24 — ManualStrategy Community Tier*

View File

@ -34,12 +34,14 @@ HEADERS += \
$$PWD/schedulingstrategies/ischedulingstrategy.h \
$$PWD/schedulingstrategies/rulebasedstrategy.h \
$$PWD/schedulingstrategies/aistrategy.h \
$$PWD/schedulingstrategies/manualstrategy.h \
$$PWD/types/chargingaction.h \
$$PWD/types/charginginfo.h \
$$PWD/types/chargingprocessinfo.h \
$$PWD/types/chargingschedule.h \
$$PWD/types/energytimeslot.h \
$$PWD/types/flexibleload.h \
$$PWD/types/manualslotconfig.h \
$$PWD/types/schedulerconfig.h \
$$PWD/types/scoreentry.h \
$$PWD/types/smartchargingstate.h \
@ -59,12 +61,14 @@ SOURCES += \
$$PWD/spotmarket/spotmarketmanager.cpp \
$$PWD/schedulingstrategies/rulebasedstrategy.cpp \
$$PWD/schedulingstrategies/aistrategy.cpp \
$$PWD/schedulingstrategies/manualstrategy.cpp \
$$PWD/types/chargingaction.cpp \
$$PWD/types/charginginfo.cpp \
$$PWD/types/chargingprocessinfo.cpp \
$$PWD/types/chargingschedule.cpp \
$$PWD/types/energytimeslot.cpp \
$$PWD/types/flexibleload.cpp \
$$PWD/types/manualslotconfig.cpp \
$$PWD/types/schedulerconfig.cpp \
$$PWD/types/scoreentry.cpp \
$$PWD/types/smartchargingstate.cpp \

View File

@ -28,6 +28,7 @@
#include "nymeaenergyjsonhandler.h"
#include "energymanagerconfiguration.h"
#include "spotmarket/spotmarketmanager.h"
#include "schedulingstrategies/manualstrategy.h"
#include "plugininfo.h"
@ -48,7 +49,10 @@ void EnergyPluginNymea::init()
SchedulerManager *schedulerManager = new SchedulerManager(spotMarketManager, energyManager(), thingManager(), this);
// Community-tier strategies (always available, no feature flag)
schedulerManager->registerStrategy(new ManualStrategy(this));
jsonRpcServer()->registerExperienceHandler(
new NymeaEnergyJsonHandler(spotMarketManager, chargingManager, schedulerManager, this),
0, 10);
0, 11);
}

View File

@ -27,6 +27,7 @@
#include "smartchargingmanager.h"
#include "spotmarket/spotmarketmanager.h"
#include "schedulermanager.h"
#include "schedulingstrategies/manualstrategy.h"
#include <energymanager.h>
@ -376,7 +377,76 @@ NymeaEnergyJsonHandler::NymeaEnergyJsonHandler(SpotMarketManager *spotMarketMana
p.insert("slot", slotToVariantMap(slot));
p.insert("appliedCommands", QVariantList());
emit SlotActivated(p);
// Emit ManualSlotActivated when the executed slot was driven by ManualStrategy
if (!slot.decisionRules.contains(QStringLiteral("ManualSlot")))
return;
ManualStrategy *ms = manualStrategy();
ManualSlotConfig matchedConfig;
if (ms) {
foreach (const ManualSlotConfig &cfg, ms->manualSlots()) {
if (cfg.matchesSlot(slot.start)) {
matchedConfig = cfg;
break;
}
}
}
QVariantMap allocs;
allocs.insert("ev", slot.allocatedToEV);
allocs.insert("battery", slot.allocatedToBattery);
allocs.insert("heatpump", slot.allocatedToHP);
allocs.insert("dhw", slot.allocatedToDHW);
allocs.insert("feedin", slot.allocatedToFeedIn);
const QString reason = matchedConfig.label.isEmpty()
? QStringLiteral("Créneau manuel activé")
: QString("Créneau manuel '%1' activé").arg(matchedConfig.label);
QVariantMap mp;
mp.insert("slot", matchedConfig.toJson());
mp.insert("appliedAllocations", allocs);
mp.insert("reason", reason);
emit ManualSlotActivated(mp);
});
// Manual slot methods (v11)
params.clear(); returns.clear();
description = "Get all manually configured scheduling slots.";
returns.insert("slots", QVariantList());
registerMethod("GetManualSlots", description, params, returns,
Types::PermissionScopeControlThings);
params.clear(); returns.clear();
description = "Create or update a manual scheduling slot. "
"When repeating=true the slot recurs weekly based on day-of-week and time.";
params.insert("start", enumValueName(String));
params.insert("end", enumValueName(String));
params.insert("label", enumValueName(String));
params.insert("repeating", enumValueName(Bool));
params.insert("o:expiresAt",enumValueName(String));
params.insert("allocations",QVariantMap());
returns.insert("energyError", enumRef<EnergyManager::EnergyError>());
registerMethod("SetManualSlot", description, params, returns,
Types::PermissionScopeControlThings);
params.clear(); returns.clear();
description = "Remove the manual slot that starts at the given UTC timestamp.";
params.insert("start", enumValueName(String));
returns.insert("energyError", enumRef<EnergyManager::EnergyError>());
registerMethod("RemoveManualSlot", description, params, returns,
Types::PermissionScopeControlThings);
params.clear(); returns.clear();
description = "Remove all manually configured scheduling slots.";
returns.insert("energyError", enumRef<EnergyManager::EnergyError>());
registerMethod("ClearManualSlots", description, params, returns,
Types::PermissionScopeControlThings);
// ManualSlotActivated push notification
params.clear();
description = "Emitted when the Scheduler executes a slot driven by ManualStrategy.";
params.insert("slot", QVariantMap());
params.insert("appliedAllocations", QVariantMap());
params.insert("reason", enumValueName(String));
registerNotification("ManualSlotActivated", description, params);
}
}
@ -723,6 +793,67 @@ JsonReply *NymeaEnergyJsonHandler::OverrideSlot(const QVariantMap &params)
return createReply({{"energyError", enumValueName(EnergyManager::EnergyErrorNoError)}});
}
// ---------------------------------------------------------------------------
// Manual slot API — NymeaEnergy v11
// ---------------------------------------------------------------------------
JsonReply *NymeaEnergyJsonHandler::GetManualSlots(const QVariantMap &params)
{
Q_UNUSED(params)
QVariantList configList;
if (m_schedulerManager) {
foreach (const ManualSlotConfig &cfg, m_schedulerManager->manualSlots())
configList.append(cfg.toJson());
}
return createReply({{"slots", configList}});
}
JsonReply *NymeaEnergyJsonHandler::SetManualSlot(const QVariantMap &params)
{
if (!m_schedulerManager)
return createReply({{"energyError", enumValueName(EnergyManager::EnergyErrorInvalidParameter)}});
ManualSlotConfig cfg = ManualSlotConfig::fromJson(params);
if (!cfg.start.isValid() || !cfg.end.isValid() || cfg.end <= cfg.start)
return createReply({{"energyError", enumValueName(EnergyManager::EnergyErrorInvalidParameter)}});
m_schedulerManager->setManualSlot(cfg);
return createReply({{"energyError", enumValueName(EnergyManager::EnergyErrorNoError)}});
}
JsonReply *NymeaEnergyJsonHandler::RemoveManualSlot(const QVariantMap &params)
{
if (!m_schedulerManager)
return createReply({{"energyError", enumValueName(EnergyManager::EnergyErrorInvalidParameter)}});
QDateTime start = QDateTime::fromString(
params.value("start").toString(), Qt::ISODateWithMs).toUTC();
if (!start.isValid())
return createReply({{"energyError", enumValueName(EnergyManager::EnergyErrorInvalidParameter)}});
m_schedulerManager->removeManualSlot(start);
return createReply({{"energyError", enumValueName(EnergyManager::EnergyErrorNoError)}});
}
JsonReply *NymeaEnergyJsonHandler::ClearManualSlots(const QVariantMap &params)
{
Q_UNUSED(params)
if (m_schedulerManager)
m_schedulerManager->clearManualSlots();
return createReply({{"energyError", enumValueName(EnergyManager::EnergyErrorNoError)}});
}
ManualStrategy *NymeaEnergyJsonHandler::manualStrategy() const
{
if (!m_schedulerManager)
return nullptr;
foreach (ISchedulingStrategy *s, m_schedulerManager->availableStrategies()) {
if (s->strategyId() == QLatin1String("manual"))
return qobject_cast<ManualStrategy *>(s);
}
return nullptr;
}
// ---------------------------------------------------------------------------
// Private helpers
// ---------------------------------------------------------------------------

View File

@ -31,6 +31,7 @@
#include "types/scoreentry.h"
#include "types/energytimeslot.h"
#include "types/manualslotconfig.h"
class SmartChargingManager;
class SpotMarketManager;
@ -82,6 +83,12 @@ public:
Q_INVOKABLE JsonReply *SetLoadConfig(const QVariantMap &params);
Q_INVOKABLE JsonReply *OverrideSlot(const QVariantMap &params);
// --- Manual slot API (NymeaEnergy v11) ---
Q_INVOKABLE JsonReply *GetManualSlots(const QVariantMap &params);
Q_INVOKABLE JsonReply *SetManualSlot(const QVariantMap &params);
Q_INVOKABLE JsonReply *RemoveManualSlot(const QVariantMap &params);
Q_INVOKABLE JsonReply *ClearManualSlots(const QVariantMap &params);
signals:
void PhasePowerLimitChanged(const QVariantMap &params);
void AcquisitionToleranceChanged(const QVariantMap &params);
@ -99,6 +106,9 @@ signals:
void SlotActivated(const QVariantMap &params);
void OverrideConflict(const QVariantMap &params);
// Manual slot push notification
void ManualSlotActivated(const QVariantMap &params);
private:
SpotMarketManager *m_spotMarketManager;
SmartChargingManager *m_smartChargingManager = nullptr;
@ -110,6 +120,9 @@ private:
// Helper: convert a timeline slot to QVariantMap for JSON-RPC
QVariantMap slotToVariantMap(const EnergyTimeSlot &slot) const;
// Helper: access the registered ManualStrategy (null if not registered)
class ManualStrategy *manualStrategy() const;
};
#endif // NYMEAENERGYJSONHANDLER_H

View File

@ -25,6 +25,8 @@
#include "schedulermanager.h"
#include "schedulingstrategies/rulebasedstrategy.h"
#include "schedulingstrategies/aistrategy.h"
#include "schedulingstrategies/manualstrategy.h"
#include "schedulersettings.h"
#include "spotmarket/spotmarketmanager.h"
#include <QLoggingCategory>
@ -40,6 +42,8 @@ SchedulerManager::SchedulerManager(
m_energyManager(energyManager),
m_thingManager(thingManager)
{
m_settings = new SchedulerSettings(this);
// Register built-in strategies
RuleBasedStrategy *ruleStrategy = new RuleBasedStrategy(this);
AIStrategy *aiStrategy = new AIStrategy(this);
@ -99,6 +103,18 @@ void SchedulerManager::registerStrategy(ISchedulingStrategy *strategy)
strategy->setParent(this);
m_strategies.append(strategy);
qCDebug(dcNymeaEnergy()) << "SchedulerManager: registered strategy" << strategy->strategyId();
// Hydrate ManualStrategy with persisted slots as soon as it is registered
if (strategy->strategyId() == QLatin1String("manual") && m_settings) {
ManualStrategy *ms = qobject_cast<ManualStrategy *>(strategy);
if (ms) {
foreach (const ManualSlotConfig &cfg, m_settings->manualSlots())
ms->setManualSlot(cfg);
qCDebug(dcNymeaEnergy()) << "SchedulerManager: loaded"
<< m_settings->manualSlots().size()
<< "manual slot(s) from settings";
}
}
}
// --- Timeline access ---
@ -212,6 +228,55 @@ int SchedulerManager::activeOverridesCount() const
return count;
}
// --- Manual slot management ---
QList<ManualSlotConfig> SchedulerManager::manualSlots() const
{
ManualStrategy *ms = findManualStrategy();
if (!ms)
return {};
return ms->manualSlots();
}
void SchedulerManager::setManualSlot(const ManualSlotConfig &config)
{
ManualStrategy *ms = findManualStrategy();
if (!ms) {
qCWarning(dcNymeaEnergy())
<< "SchedulerManager::setManualSlot: no ManualStrategy registered";
return;
}
ms->setManualSlot(config);
if (m_settings)
m_settings->setManualSlot(config);
emit manualSlotsChanged();
forceRecompute();
}
void SchedulerManager::removeManualSlot(const QDateTime &start)
{
ManualStrategy *ms = findManualStrategy();
if (!ms)
return;
ms->removeManualSlot(start);
if (m_settings)
m_settings->removeManualSlot(start);
emit manualSlotsChanged();
forceRecompute();
}
void SchedulerManager::clearManualSlots()
{
ManualStrategy *ms = findManualStrategy();
if (!ms)
return;
ms->clearAllManualSlots();
if (m_settings)
m_settings->clearManualSlots();
emit manualSlotsChanged();
forceRecompute();
}
// --- Force recompute ---
void SchedulerManager::forceRecompute()
@ -339,6 +404,15 @@ void SchedulerManager::applyCurrentSlot(const EnergyTimeSlot &slot)
<< "Bat=" << slot.allocatedToBattery << "W";
}
ManualStrategy *SchedulerManager::findManualStrategy() const
{
foreach (ISchedulingStrategy *s, m_strategies) {
if (s->strategyId() == QLatin1String("manual"))
return qobject_cast<ManualStrategy *>(s);
}
return nullptr;
}
void SchedulerManager::scheduleNextSlotTimer()
{
m_slotTimer.stop();

View File

@ -33,6 +33,7 @@
#include "types/energytimeslot.h"
#include "types/flexibleload.h"
#include "types/schedulerconfig.h"
#include "types/manualslotconfig.h"
#include "schedulingstrategies/ischedulingstrategy.h"
// from libnymea-energy
@ -99,6 +100,12 @@ public:
// --- Force immediate recompute ---
void forceRecompute();
// --- Manual slot management (ManualStrategy) ---
QList<ManualSlotConfig> manualSlots() const;
void setManualSlot(const ManualSlotConfig &config);
void removeManualSlot(const QDateTime &start);
void clearManualSlots();
signals:
void timelineUpdated(const QList<EnergyTimeSlot> &timeline);
void slotExecuted(const EnergyTimeSlot &slot, bool success);
@ -106,6 +113,7 @@ signals:
void loadRegistered(const FlexibleLoad &load);
void loadUpdated(const FlexibleLoad &load);
void configChanged(const SchedulerConfig &config);
void manualSlotsChanged();
private slots:
void onRecomputeTimer();
@ -124,9 +132,13 @@ private:
// Schedule next slot execution timer
void scheduleNextSlotTimer();
// Helper: return the registered ManualStrategy, or nullptr if not found
class ManualStrategy *findManualStrategy() const;
SpotMarketManager *m_spotMarketManager = nullptr;
EnergyManager *m_energyManager = nullptr;
ThingManager *m_thingManager = nullptr;
class SchedulerSettings *m_settings = nullptr;
ISchedulingStrategy *m_activeStrategy = nullptr;
QList<ISchedulingStrategy *> m_strategies;

View File

@ -23,6 +23,7 @@
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include "schedulersettings.h"
#include "types/manualslotconfig.h"
#include <nymeasettings.h>
@ -134,6 +135,47 @@ void SchedulerSettings::clearExpiredOverrides()
save();
}
// --- Manual slots ---
QList<ManualSlotConfig> SchedulerSettings::manualSlots() const
{
return m_manualSlots;
}
void SchedulerSettings::setManualSlot(const ManualSlotConfig &config)
{
const QString key = config.start.toUTC().toString(Qt::ISODateWithMs);
for (int i = 0; i < m_manualSlots.size(); ++i) {
if (m_manualSlots.at(i).start.toUTC().toString(Qt::ISODateWithMs) == key) {
m_manualSlots[i] = config;
save();
return;
}
}
m_manualSlots.append(config);
save();
}
void SchedulerSettings::removeManualSlot(const QDateTime &start)
{
const QString key = start.toUTC().toString(Qt::ISODateWithMs);
for (int i = 0; i < m_manualSlots.size(); ++i) {
if (m_manualSlots.at(i).start.toUTC().toString(Qt::ISODateWithMs) == key) {
m_manualSlots.removeAt(i);
save();
return;
}
}
}
void SchedulerSettings::clearManualSlots()
{
if (m_manualSlots.isEmpty())
return;
m_manualSlots.clear();
save();
}
// --- Persistence ---
void SchedulerSettings::load()
@ -183,7 +225,44 @@ void SchedulerSettings::load()
}
s.endArray();
qCDebug(dcNymeaEnergy()) << "SchedulerSettings: loaded from" << settingsFilePath();
// Load manual slots
m_manualSlots.clear();
int manualCount = s.beginReadArray(QStringLiteral("manualSlots"));
for (int i = 0; i < manualCount; ++i) {
s.setArrayIndex(i);
ManualSlotConfig cfg;
cfg.start = QDateTime::fromString(s.value("start").toString(), Qt::ISODateWithMs).toUTC();
cfg.end = QDateTime::fromString(s.value("end").toString(), Qt::ISODateWithMs).toUTC();
cfg.label = s.value("label").toString();
cfg.repeating = s.value("repeating", false).toBool();
const QString expStr = s.value("expiresAt").toString();
if (!expStr.isEmpty())
cfg.expiresAt = QDateTime::fromString(expStr, Qt::ISODateWithMs).toUTC();
// Allocations stored as alloc_ev, alloc_battery, …
const QStringList allocKeys = {
QStringLiteral("ev"), QStringLiteral("battery"),
QStringLiteral("dhw"), QStringLiteral("heatpump"), QStringLiteral("feedin")
};
foreach (const QString &key, allocKeys) {
double val = s.value(QStringLiteral("alloc_") + key, 0.0).toDouble();
if (val != 0.0)
cfg.powerAllocations.insert(manualSlotSourceFromKey(key), val);
}
if (!cfg.start.isValid()) continue;
if (cfg.isExpired()) {
qCDebug(dcNymeaEnergy()) << "SchedulerSettings: discarding expired manual slot"
<< cfg.label << "(expired" << cfg.expiresAt << ")";
continue;
}
m_manualSlots.append(cfg);
}
s.endArray();
qCDebug(dcNymeaEnergy()) << "SchedulerSettings: loaded from" << settingsFilePath()
<< "" << m_manualSlots.size() << "manual slot(s)";
}
void SchedulerSettings::save()
@ -224,4 +303,23 @@ void SchedulerSettings::save()
s.setValue("expiresAt", it.value().expiresAt.toUTC().toString(Qt::ISODateWithMs));
}
s.endArray();
s.beginWriteArray(QStringLiteral("manualSlots"), m_manualSlots.size());
idx = 0;
foreach (const ManualSlotConfig &cfg, m_manualSlots) {
s.setArrayIndex(idx++);
s.setValue("start", cfg.start.toUTC().toString(Qt::ISODateWithMs));
s.setValue("end", cfg.end.toUTC().toString(Qt::ISODateWithMs));
s.setValue("label", cfg.label);
s.setValue("repeating", cfg.repeating);
if (cfg.expiresAt.isValid())
s.setValue("expiresAt", cfg.expiresAt.toUTC().toString(Qt::ISODateWithMs));
// Allocations
for (auto it = cfg.powerAllocations.constBegin();
it != cfg.powerAllocations.constEnd(); ++it) {
s.setValue(QStringLiteral("alloc_") + manualSlotAllocationKey(it.key()),
it.value());
}
}
s.endArray();
}

View File

@ -32,6 +32,7 @@
#include "types/schedulerconfig.h"
#include "types/flexibleload.h"
#include "types/energytimeslot.h"
#include "types/manualslotconfig.h"
// Persists SchedulerManager mutable state to:
// NymeaSettings::settingsPath() + "/scheduler.conf" (QSettings INI)
@ -80,6 +81,12 @@ public:
void removeOverride(const QDateTime &slotStart);
void clearExpiredOverrides();
// Manual slot configurations (used by ManualStrategy)
QList<ManualSlotConfig> manualSlots() const;
void setManualSlot(const ManualSlotConfig &config);
void removeManualSlot(const QDateTime &start);
void clearManualSlots();
private:
QString settingsFilePath() const;
@ -87,6 +94,7 @@ private:
SchedulerConfig m_config;
QHash<QString, LoadConfig> m_loadConfigs;
QHash<QString, OverrideEntry> m_overrides;
QList<ManualSlotConfig> m_manualSlots;
void load();
void save();

View File

@ -0,0 +1,210 @@
// SPDX-License-Identifier: GPL-3.0-or-later
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright (C) 2013 - 2024, nymea GmbH
* Copyright (C) 2024 - 2025, chargebyte austria GmbH
*
* This file is part of nymea-energy-plugin-nymea.
*
* nymea-energy-plugin-nymea.s free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* nymea-energy-plugin-nymea.s distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with nymea-energy-plugin-nymea. If not, see <https://www.gnu.org/licenses/>.
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include "manualstrategy.h"
#include <QLoggingCategory>
Q_DECLARE_LOGGING_CATEGORY(dcNymeaEnergy)
ManualStrategy::ManualStrategy(QObject *parent)
: ISchedulingStrategy(parent)
{
}
// ---------------------------------------------------------------------------
// computeSchedule
// ---------------------------------------------------------------------------
QList<EnergyTimeSlot> ManualStrategy::computeSchedule(
const QList<EnergyTimeSlot> &forecast,
const QList<FlexibleLoad> &loads,
const SchedulerConfig &config)
{
Q_UNUSED(config)
QList<EnergyTimeSlot> timeline = forecast;
for (int i = 0; i < timeline.size(); ++i) {
EnergyTimeSlot &slot = timeline[i];
if (slot.manualOverride)
continue; // respect human decisions
// Clear previous allocations
slot.allocatedToEV = 0;
slot.allocatedToHP = 0;
slot.allocatedToDHW = 0;
slot.allocatedToBattery = 0;
slot.allocatedToFeedIn = 0;
slot.decisionReason.clear();
slot.decisionRules.clear();
// Look for a matching manual config
const ManualSlotConfig *matched = nullptr;
bool expiredMatch = false;
foreach (const ManualSlotConfig &cfg, m_manualSlots) {
if (!cfg.matchesSlotIgnoreExpiry(slot.start))
continue;
if (cfg.isExpired()) {
expiredMatch = true;
qCDebug(dcNymeaEnergy())
<< "ManualStrategy: expired config" << cfg.label
<< "— skipped for slot" << slot.start;
} else {
matched = &cfg;
break;
}
}
if (matched) {
// Apply exactly the user-defined allocations
slot.allocatedToEV = matched->powerAllocations.value(LoadSource::SmartCharging, 0);
slot.allocatedToHP = matched->powerAllocations.value(LoadSource::HeatPump, 0);
slot.allocatedToDHW = matched->powerAllocations.value(LoadSource::DHW, 0);
slot.allocatedToBattery = matched->powerAllocations.value(LoadSource::Battery, 0);
slot.allocatedToFeedIn = matched->powerAllocations.value(LoadSource::FeedIn, 0);
if (!matched->label.isEmpty())
slot.decisionReason =
QString("Créneau manuel '%1' configuré par l'utilisateur")
.arg(matched->label);
else
slot.decisionReason =
QStringLiteral("Créneau manuel configuré par l'utilisateur");
slot.decisionRules.append(QStringLiteral("ManualSlot"));
} else if (expiredMatch) {
// Apply safe fallback first, then override reason to show expiry
applyInflexibleLoads(slot, loads);
slot.decisionReason = QStringLiteral("Créneau expiré — ignoré");
if (!slot.decisionRules.contains(QStringLiteral("ExpiredSlot")))
slot.decisionRules.prepend(QStringLiteral("ExpiredSlot"));
} else {
applyInflexibleLoads(slot, loads);
// applyInflexibleLoads sets reason if critical loads were found;
// fall back to generic message if still empty.
if (slot.decisionReason.isEmpty()) {
slot.decisionReason =
QStringLiteral("Aucune configuration — charges critiques uniquement");
slot.decisionRules.append(QStringLiteral("ManualDefault"));
}
}
}
return timeline;
}
// ---------------------------------------------------------------------------
// explainDecision
// ---------------------------------------------------------------------------
QString ManualStrategy::explainDecision(
const EnergyTimeSlot &slot,
const FlexibleLoad &load) const
{
Q_UNUSED(load)
if (slot.manualOverride)
return QStringLiteral("Décision manuelle : ") + slot.overrideReason;
if (slot.decisionRules.contains(QStringLiteral("ManualSlot")))
return slot.decisionReason;
if (slot.decisionRules.contains(QStringLiteral("ExpiredSlot")))
return QStringLiteral("La configuration de ce créneau a expiré — "
"seules les charges critiques restent actives.");
return QStringLiteral("Aucune configuration manuelle pour ce créneau.");
}
// ---------------------------------------------------------------------------
// Manual slot management
// ---------------------------------------------------------------------------
void ManualStrategy::setManualSlot(const ManualSlotConfig &slotConfig)
{
for (int i = 0; i < m_manualSlots.size(); ++i) {
if (m_manualSlots.at(i).start == slotConfig.start) {
m_manualSlots[i] = slotConfig;
return;
}
}
m_manualSlots.append(slotConfig);
}
void ManualStrategy::removeManualSlot(const QDateTime &slotStart)
{
for (int i = 0; i < m_manualSlots.size(); ++i) {
if (m_manualSlots.at(i).start == slotStart) {
m_manualSlots.removeAt(i);
return;
}
}
}
void ManualStrategy::clearAllManualSlots()
{
m_manualSlots.clear();
}
QList<ManualSlotConfig> ManualStrategy::manualSlots() const
{
return m_manualSlots;
}
// ---------------------------------------------------------------------------
// Private helper
// ---------------------------------------------------------------------------
void ManualStrategy::applyInflexibleLoads(EnergyTimeSlot &slot,
const QList<FlexibleLoad> &loads) const
{
foreach (const FlexibleLoad &load, loads) {
if (load.type != LoadType::Inflexible)
continue;
if (load.source == LoadSource::HeatPump && load.priority >= 0.9) {
slot.allocatedToHP = qMax(slot.allocatedToHP, load.currentPowerW);
if (slot.decisionReason.isEmpty())
slot.decisionReason =
QStringLiteral("Chauffage critique — PAC toujours active");
if (!slot.decisionRules.contains(QStringLiteral("CriticalHeating")))
slot.decisionRules.append(QStringLiteral("CriticalHeating"));
}
if (load.source == LoadSource::DHW && load.priority >= 0.9) {
slot.allocatedToDHW = qMax(slot.allocatedToDHW, load.currentPowerW);
if (slot.decisionReason.isEmpty())
slot.decisionReason =
QStringLiteral("ECS critique (sécurité légionellose)");
if (!slot.decisionRules.contains(QStringLiteral("CriticalDHW")))
slot.decisionRules.append(QStringLiteral("CriticalDHW"));
}
}
}

View File

@ -0,0 +1,80 @@
// SPDX-License-Identifier: GPL-3.0-or-later
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright (C) 2013 - 2024, nymea GmbH
* Copyright (C) 2024 - 2025, chargebyte austria GmbH
*
* This file is part of nymea-energy-plugin-nymea.
*
* nymea-energy-plugin-nymea.s free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* nymea-energy-plugin-nymea.s distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with nymea-energy-plugin-nymea. If not, see <https://www.gnu.org/licenses/>.
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#ifndef MANUALSTRATEGY_H
#define MANUALSTRATEGY_H
#include "ischedulingstrategy.h"
#include "types/manualslotconfig.h"
// Community-tier scheduling strategy: the user defines all time windows and
// power allocations explicitly.
//
// For each forecast slot:
// - If a ManualSlotConfig covers that slot: apply its allocations exactly.
// - If a matching config exists but is expired: reason = "Créneau expiré — ignoré",
// apply critical/inflexible loads for safety.
// - If no config matches: apply critical/inflexible loads only, reason =
// "Aucune configuration — charges critiques uniquement".
//
// decisionReason is never empty (contract from ISchedulingStrategy).
class ManualStrategy : public ISchedulingStrategy
{
Q_OBJECT
public:
explicit ManualStrategy(QObject *parent = nullptr);
QString strategyId() const override { return QStringLiteral("manual"); }
QString displayName() const override { return QStringLiteral("Manuel"); }
QString description() const override {
return QStringLiteral(
"L'utilisateur définit lui-même les créneaux de charge. "
"Aucune automatisation — contrôle total.");
}
QList<EnergyTimeSlot> computeSchedule(
const QList<EnergyTimeSlot> &forecast,
const QList<FlexibleLoad> &loads,
const SchedulerConfig &config) override;
QString explainDecision(
const EnergyTimeSlot &slot,
const FlexibleLoad &load) const override;
// Manual slot management
void setManualSlot(const ManualSlotConfig &slotConfig);
void removeManualSlot(const QDateTime &slotStart);
void clearAllManualSlots();
QList<ManualSlotConfig> manualSlots() const;
private:
// Apply critical/inflexible loads to a slot (safety fallback).
// Sets decisionReason if it is still empty after processing.
void applyInflexibleLoads(EnergyTimeSlot &slot,
const QList<FlexibleLoad> &loads) const;
QList<ManualSlotConfig> m_manualSlots;
};
#endif // MANUALSTRATEGY_H

View File

@ -0,0 +1,153 @@
// SPDX-License-Identifier: GPL-3.0-or-later
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright (C) 2013 - 2024, nymea GmbH
* Copyright (C) 2024 - 2025, chargebyte austria GmbH
*
* This file is part of nymea-energy-plugin-nymea.
*
* nymea-energy-plugin-nymea.s free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* nymea-energy-plugin-nymea.s distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with nymea-energy-plugin-nymea. If not, see <https://www.gnu.org/licenses/>.
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include "manualslotconfig.h"
// ---------------------------------------------------------------------------
// Free helpers for powerAllocations map serialisation
// ---------------------------------------------------------------------------
QString manualSlotAllocationKey(LoadSource source)
{
switch (source) {
case LoadSource::SmartCharging: return QStringLiteral("ev");
case LoadSource::Battery: return QStringLiteral("battery");
case LoadSource::DHW: return QStringLiteral("dhw");
case LoadSource::HeatPump: return QStringLiteral("heatpump");
case LoadSource::FeedIn: return QStringLiteral("feedin");
default: return QStringLiteral("external");
}
}
LoadSource manualSlotSourceFromKey(const QString &key)
{
if (key == QLatin1String("ev")) return LoadSource::SmartCharging;
if (key == QLatin1String("battery")) return LoadSource::Battery;
if (key == QLatin1String("dhw")) return LoadSource::DHW;
if (key == QLatin1String("heatpump")) return LoadSource::HeatPump;
if (key == QLatin1String("feedin")) return LoadSource::FeedIn;
return LoadSource::External;
}
// ---------------------------------------------------------------------------
// Expiry check
// ---------------------------------------------------------------------------
bool ManualSlotConfig::isExpired() const
{
return expiresAt.isValid()
&& expiresAt <= QDateTime::currentDateTimeUtc();
}
// ---------------------------------------------------------------------------
// Slot matching
// ---------------------------------------------------------------------------
// Compute "minutes since start of week" for repeating-slot comparison.
// Qt dayOfWeek(): 1=Mon … 7=Sun. We convert to 0-based (Mon=0).
static int minsOfWeek(const QDateTime &dt)
{
QDateTime utc = dt.toUTC();
int dayIndex = utc.date().dayOfWeek() - 1; // 0=Mon … 6=Sun
return dayIndex * 24 * 60
+ utc.time().hour() * 60
+ utc.time().minute();
}
bool ManualSlotConfig::matchesSlotIgnoreExpiry(const QDateTime &slotStart) const
{
if (!start.isValid() || !end.isValid())
return false;
if (!repeating)
return slotStart >= start && slotStart < end;
// Repeating: compare minutes-of-week for day-of-week + time-of-day
int slotMins = minsOfWeek(slotStart);
int startMins = minsOfWeek(start);
int endMins = minsOfWeek(end);
if (startMins <= endMins) {
// Normal case — stays within the same calendar week segment
return slotMins >= startMins && slotMins < endMins;
} else {
// Wrap-around case — e.g. Sun 22:00 → Mon 06:00
return slotMins >= startMins || slotMins < endMins;
}
}
bool ManualSlotConfig::matchesSlot(const QDateTime &slotStart) const
{
if (isExpired())
return false;
return matchesSlotIgnoreExpiry(slotStart);
}
// ---------------------------------------------------------------------------
// JSON serialisation
// ---------------------------------------------------------------------------
QVariantMap ManualSlotConfig::toJson() const
{
QVariantMap map;
if (start.isValid())
map.insert("start", start.toUTC().toString(Qt::ISODateWithMs));
if (end.isValid())
map.insert("end", end.toUTC().toString(Qt::ISODateWithMs));
map.insert("label", label);
map.insert("repeating", repeating);
if (expiresAt.isValid())
map.insert("expiresAt", expiresAt.toUTC().toString(Qt::ISODateWithMs));
QVariantMap allocs;
for (auto it = powerAllocations.constBegin(); it != powerAllocations.constEnd(); ++it)
allocs.insert(manualSlotAllocationKey(it.key()), it.value());
map.insert("allocations", allocs);
return map;
}
ManualSlotConfig ManualSlotConfig::fromJson(const QVariantMap &map)
{
ManualSlotConfig cfg;
cfg.start = QDateTime::fromString(
map.value("start").toString(), Qt::ISODateWithMs).toUTC();
cfg.end = QDateTime::fromString(
map.value("end").toString(), Qt::ISODateWithMs).toUTC();
cfg.label = map.value("label").toString();
cfg.repeating = map.value("repeating", false).toBool();
const QString expiresStr = map.value("expiresAt").toString();
if (!expiresStr.isEmpty())
cfg.expiresAt = QDateTime::fromString(expiresStr, Qt::ISODateWithMs).toUTC();
const QVariantMap allocs = map.value("allocations").toMap();
for (auto it = allocs.constBegin(); it != allocs.constEnd(); ++it) {
LoadSource src = manualSlotSourceFromKey(it.key());
if (src != LoadSource::External || it.key() == QLatin1String("external"))
cfg.powerAllocations.insert(src, it.value().toDouble());
}
return cfg;
}

View File

@ -0,0 +1,70 @@
// SPDX-License-Identifier: GPL-3.0-or-later
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
* Copyright (C) 2013 - 2024, nymea GmbH
* Copyright (C) 2024 - 2025, chargebyte austria GmbH
*
* This file is part of nymea-energy-plugin-nymea.
*
* nymea-energy-plugin-nymea.s free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* nymea-energy-plugin-nymea.s distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with nymea-energy-plugin-nymea. If not, see <https://www.gnu.org/licenses/>.
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#ifndef MANUALSLOTCONFIG_H
#define MANUALSLOTCONFIG_H
#include <QDateTime>
#include <QMap>
#include <QString>
#include <QVariant>
#include <QMetaType>
#include "flexibleload.h"
// Configuration for one manually-defined scheduling slot.
// Used exclusively by ManualStrategy.
//
// When repeating = true, the config recurs weekly based on the day-of-week
// and time-of-day derived from start/end. Overnight slots (e.g. Mon 22:00
// → Tue 06:00) are handled correctly.
struct ManualSlotConfig {
QDateTime start;
QDateTime end;
QMap<LoadSource, double> powerAllocations; // LoadSource → Watts
QString label; // user-visible name, e.g. "Recharge VE nuit"
bool repeating = false;
QDateTime expiresAt; // optional — slot is ignored after this time
bool isNull() const { return !start.isValid(); }
bool isExpired() const;
// Returns true if slotStart falls within this config's time window.
// Returns false when isExpired() — use matchesSlotIgnoreExpiry() to detect
// "would have matched but is expired".
bool matchesSlot(const QDateTime &slotStart) const;
bool matchesSlotIgnoreExpiry(const QDateTime &slotStart) const;
QVariantMap toJson() const;
static ManualSlotConfig fromJson(const QVariantMap &map);
};
Q_DECLARE_METATYPE(ManualSlotConfig)
// Helpers for serialising powerAllocations map.
// Keys used in JSON: "ev", "battery", "dhw", "heatpump", "feedin"
QString manualSlotAllocationKey(LoadSource source);
LoadSource manualSlotSourceFromKey(const QString &key);
#endif // MANUALSLOTCONFIG_H

View File

@ -334,6 +334,221 @@ void TestScheduler::testHotStrategySwap()
"AI stub missing AIModelNotLoaded rule tag");
}
// ---------------------------------------------------------------------------
// Test 5 — ManualStrategy: slot configured → allocations applied exactly
// ---------------------------------------------------------------------------
void TestScheduler::testManualStrategy_basicSlot()
{
// Slot: today 22:00 UTC → tomorrow 06:00 UTC, ev=2000W, battery=1000W
QDate today = QDate::currentDate();
QDateTime slotStart = QDateTime(today, QTime(22, 0, 0), Qt::UTC);
QDateTime slotEnd = QDateTime(today.addDays(1), QTime(6, 0, 0), Qt::UTC);
ManualSlotConfig cfg;
cfg.start = slotStart;
cfg.end = slotEnd;
cfg.label = QStringLiteral("Recharge VE nuit");
cfg.repeating = false;
cfg.powerAllocations.insert(LoadSource::SmartCharging, 2000.0);
cfg.powerAllocations.insert(LoadSource::Battery, 1000.0);
ManualStrategy strategy;
strategy.setManualSlot(cfg);
// Build a 2-slot forecast covering 22:00 and 23:00
QList<EnergyTimeSlot> forecast;
for (int h = 0; h < 2; ++h) {
EnergyTimeSlot s;
s.start = slotStart.addSecs(h * 3600);
s.end = slotStart.addSecs((h + 1) * 3600);
forecast.append(s);
}
SchedulerConfig config;
QList<EnergyTimeSlot> result = strategy.computeSchedule(forecast, {}, config);
QCOMPARE(result.size(), 2);
// Both slots fall within 22:0006:00 → allocations must match exactly
foreach (const EnergyTimeSlot &slot, result) {
QCOMPARE(slot.allocatedToEV, 2000.0);
QCOMPARE(slot.allocatedToBattery, 1000.0);
QVERIFY2(!slot.decisionReason.isEmpty(), "decisionReason must not be empty");
QVERIFY2(slot.decisionReason.contains("manuel", Qt::CaseInsensitive) ||
slot.decisionReason.contains("Recharge VE nuit"),
"decisionReason should mention manual or label");
QVERIFY2(slot.decisionRules.contains("ManualSlot"),
"decisionRules must contain ManualSlot");
}
}
// ---------------------------------------------------------------------------
// Test 6 — ManualStrategy: no config → fallback with safe reason
// ---------------------------------------------------------------------------
void TestScheduler::testManualStrategy_noConfig_fallback()
{
// No manual slot configured at all
ManualStrategy strategy;
QDateTime base = QDateTime(QDate::currentDate(), QTime(10, 0, 0), Qt::UTC);
QList<EnergyTimeSlot> forecast;
for (int h = 0; h < 3; ++h) {
EnergyTimeSlot s;
s.start = base.addSecs(h * 3600);
s.end = base.addSecs((h + 1) * 3600);
forecast.append(s);
}
SchedulerConfig config;
QList<EnergyTimeSlot> result = strategy.computeSchedule(forecast, {}, config);
QCOMPARE(result.size(), 3);
foreach (const EnergyTimeSlot &slot, result) {
QCOMPARE(slot.allocatedToEV, 0.0);
QCOMPARE(slot.allocatedToBattery, 0.0);
QVERIFY2(!slot.decisionReason.isEmpty(),
"decisionReason must never be empty");
QVERIFY2(slot.decisionReason.contains("Aucune configuration",
Qt::CaseInsensitive) ||
slot.decisionRules.contains("ManualDefault"),
"Slot without config must report ManualDefault");
}
}
// ---------------------------------------------------------------------------
// Test 7 — ManualStrategy: expired slot is ignored, reason set correctly
// ---------------------------------------------------------------------------
void TestScheduler::testManualStrategy_expiredSlot()
{
QDate today = QDate::currentDate();
QDateTime slotStart = QDateTime(today, QTime(14, 0, 0), Qt::UTC);
QDateTime slotEnd = QDateTime(today, QTime(16, 0, 0), Qt::UTC);
ManualSlotConfig cfg;
cfg.start = slotStart;
cfg.end = slotEnd;
cfg.label = QStringLiteral("Créneau dépassé");
cfg.repeating = false;
// Expiry = yesterday → already expired
cfg.expiresAt = QDateTime::currentDateTimeUtc().addDays(-1);
cfg.powerAllocations.insert(LoadSource::SmartCharging, 5000.0);
ManualStrategy strategy;
strategy.setManualSlot(cfg);
QList<EnergyTimeSlot> forecast;
EnergyTimeSlot s;
s.start = slotStart;
s.end = slotStart.addSecs(3600);
forecast.append(s);
SchedulerConfig config;
QList<EnergyTimeSlot> result = strategy.computeSchedule(forecast, {}, config);
QCOMPARE(result.size(), 1);
const EnergyTimeSlot &slot = result.first();
// Expired slot: EV must NOT be charged
QCOMPARE(slot.allocatedToEV, 0.0);
// Reason must clearly state expiry
QVERIFY2(slot.decisionReason.contains("expiré", Qt::CaseInsensitive),
"decisionReason must mention expiry");
QVERIFY2(slot.decisionRules.contains("ExpiredSlot"),
"decisionRules must contain ExpiredSlot");
}
// ---------------------------------------------------------------------------
// Test 8 — ManualStrategy: repeating weekly slot applied to next recurrence
// ---------------------------------------------------------------------------
void TestScheduler::testManualStrategy_repeatingSlot()
{
// Find the most recent Monday as our anchor
QDate anchor = QDate::currentDate();
while (anchor.dayOfWeek() != 1) // Qt: 1 = Monday
anchor = anchor.addDays(-1);
ManualSlotConfig cfg;
cfg.start = QDateTime(anchor, QTime(22, 0, 0), Qt::UTC);
cfg.end = QDateTime(anchor.addDays(1), QTime(6, 0, 0), Qt::UTC); // overnight
cfg.label = QStringLiteral("Recharge hebdomadaire");
cfg.repeating = true;
// No expiresAt — repeats indefinitely
cfg.powerAllocations.insert(LoadSource::SmartCharging, 3000.0);
ManualStrategy strategy;
strategy.setManualSlot(cfg);
// Next Monday 23:00 — should match the repeating window Mon 22:00 → Tue 06:00
QDate nextMonday = anchor.addDays(7);
QDateTime testSlot = QDateTime(nextMonday, QTime(23, 0, 0), Qt::UTC);
QList<EnergyTimeSlot> forecast;
EnergyTimeSlot s;
s.start = testSlot;
s.end = testSlot.addSecs(3600);
forecast.append(s);
SchedulerConfig config;
QList<EnergyTimeSlot> result = strategy.computeSchedule(forecast, {}, config);
QCOMPARE(result.size(), 1);
QVERIFY2(result.first().allocatedToEV > 0,
"Repeating slot must be applied to next weekly recurrence");
QCOMPARE(result.first().allocatedToEV, 3000.0);
QVERIFY2(result.first().decisionReason.contains("Recharge hebdomadaire"),
"decisionReason must contain the slot label");
}
// ---------------------------------------------------------------------------
// Test 9 — ManualStrategy: JSON round-trip (persistence simulation)
// ---------------------------------------------------------------------------
void TestScheduler::testManualStrategy_persistence()
{
QDate today = QDate::currentDate();
ManualSlotConfig original;
original.start = QDateTime(today, QTime(22, 0, 0), Qt::UTC);
original.end = QDateTime(today.addDays(1), QTime(6, 0, 0), Qt::UTC);
original.label = QStringLiteral("Recharge VE nuit");
original.repeating = false;
original.expiresAt = QDateTime::currentDateTimeUtc().addDays(7); // future
original.powerAllocations.insert(LoadSource::SmartCharging, 2000.0);
original.powerAllocations.insert(LoadSource::Battery, 1000.0);
// Serialize → deserialize
QVariantMap json = original.toJson();
ManualSlotConfig restored = ManualSlotConfig::fromJson(json);
QCOMPARE(restored.start, original.start);
QCOMPARE(restored.end, original.end);
QCOMPARE(restored.label, original.label);
QCOMPARE(restored.repeating, original.repeating);
QCOMPARE(restored.expiresAt, original.expiresAt);
QCOMPARE(restored.powerAllocations.value(LoadSource::SmartCharging), 2000.0);
QCOMPARE(restored.powerAllocations.value(LoadSource::Battery), 1000.0);
// Verify the restored config is functional: apply it in a new strategy instance
ManualStrategy strategy;
strategy.setManualSlot(restored);
QList<EnergyTimeSlot> forecast;
EnergyTimeSlot s;
s.start = original.start;
s.end = original.start.addSecs(3600);
forecast.append(s);
SchedulerConfig config;
QList<EnergyTimeSlot> result = strategy.computeSchedule(forecast, {}, config);
QCOMPARE(result.size(), 1);
QCOMPARE(result.first().allocatedToEV, 2000.0);
QCOMPARE(result.first().allocatedToBattery, 1000.0);
QVERIFY2(result.first().decisionRules.contains("ManualSlot"),
"Restored slot must produce ManualSlot rule after round-trip");
}
// ---------------------------------------------------------------------------
// Test runner
// ---------------------------------------------------------------------------

View File

@ -35,6 +35,8 @@
#include "types/schedulerconfig.h"
#include "schedulingstrategies/rulebasedstrategy.h"
#include "schedulingstrategies/aistrategy.h"
#include "schedulingstrategies/manualstrategy.h"
#include "types/manualslotconfig.h"
// Unit tests for the scheduling algorithm.
// These tests operate on pure algorithm logic (RuleBasedStrategy, AIStrategy)
@ -72,6 +74,21 @@ private slots:
// → No crash, plan is valid
void testHotStrategySwap();
// Test 5: ManualStrategy — slot configured, allocations applied exactly
void testManualStrategy_basicSlot();
// Test 6: ManualStrategy — no config → fallback (critical loads only)
void testManualStrategy_noConfig_fallback();
// Test 7: ManualStrategy — expired slot ignored, reason set correctly
void testManualStrategy_expiredSlot();
// Test 8: ManualStrategy — repeating weekly slot applied to next recurrence
void testManualStrategy_repeatingSlot();
// Test 9: ManualStrategy — JSON round-trip (persistence simulation)
void testManualStrategy_persistence();
private:
// Build a synthetic 24h forecast starting at a given base time
QList<EnergyTimeSlot> buildWinterForecast(const QDateTime &start) const;