Publish fixed prices independently of the dynamic price service - #1260
Open
gskjold wants to merge 2 commits into
Open
Publish fixed prices independently of the dynamic price service#1260gskjold wants to merge 2 commits into
gskjold wants to merge 2 commits into
Conversation
A fixed price is a price we always know, but publishing it to MQTT was tied to the dynamic price service in three ways: - The price point trigger in PriceService::loop() only fell back to the fixed price when fetching was disabled, so a device with fetching enabled and a failing price source never published its fixed price at all. - Day init returned without publishing, so a fixed price was not published until the next price point. - Publishing todays prices was suppressed if we were also about to fetch tomorrows, so a device that fetched today after 13:00 while tomorrow was unavailable withheld the prices it just got until the next price point. The handlers gated the whole payload on hasPrice(), which only looks at the current price point. A fixed price configured for a period or for certain hours of the day is unknown right now but known later, so nothing was published. They now gate on hasAnyPrice() and publish null for the points where a dynamic component is involved, and the Home Assistant discovery spans those holes out to the last point we know a price for. Home Assistant price sensors expired 300 seconds after their price point, which left no margin at 60 minute resolution: one missed publish took every price entity unavailable until the next hour. They now get a full extra price point of slack. Finally, disabling price fetching deleted the PriceService that also holds the fixed prices, and left EnergyAccounting with a dangling pointer to it. It is now kept alive and reconfigured, mirroring what setup() does at boot. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
When every hour of today and tomorrow is covered by a fixed price in both directions, the dynamic price cannot affect any price point, since getPricePoint() only falls back to it where no fixed price applies. Fetching it is then pure cost: a request, a possible error state and a red banner for a value that is never used. This is the configuration #1253 suggests solving by hand, by unticking the fetch option. isDynamicPriceNeeded() walks the 48 hours of today and tomorrow and asks whether a fixed price applies to each of them, accumulating the directions covered so that a split import/export configuration counts as covered. The horizon is anchored at the start of today rather than at the current price point, since the day cost is calculated backwards from midnight, and it only moves at midnight, so the answer is cached and invalidated on day change and whenever the price configuration is touched. A partial fixed price, for a period of the year or for certain hours, still needs the dynamic price and keeps fetching. A period boundary entering the horizon makes it start fetching a day ahead of needing it. Also widen getFixedPrice() to take a uint8_t point. getPricePoint() passes points up to 191 for a 15 minute two day horizon, and the int8_t parameter truncated everything from 128 up into a negative offset, so a partial fixed price resolved against a time before todays midnight for the back half of tomorrow. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
🔧 PR Build ArtifactsVersion: All environments built successfully. Download the zip files:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #1253. Also fixes the withheld-publish behaviour seen in #1252, and means the price entities in #1249 already carry fixed price + modifiers when the server is down.
In #1253 the reporter has a fixed price (Norgespris) plus modifiers, with Enable price fetch from remote server still ticked. While the price server was down nothing was published to MQTT, even though the web dashboard showed the right price. The price error itself never gated publishing — the publish trigger did, in several ways that all bite exactly when the price source is unavailable.
What was wrong
1. The fixed-price fallback only applied when fetching was disabled. Both price-point triggers in
PriceService::loop()returnedWith fetching enabled but not delivering (
today == NULL), that isfalse || (false && …). This is the reported bug: a fixed price plus modifiers and the fetch option ticked published nothing on the hour. Only the one-shot publish inMQTT_connect()got anything out, which is why a reboot did not help either — at that pointhasPrice()was consulted before any fetch had happened. Both triggers now fall back on a newhasFixedPrice()regardless ofconfig->enabled.2. Day init published nothing. The
currentDay == 0branch fell through without signalling a publish, so a fixed price was not published until the next price point — up to an hour after boot.3. Today's prices were withheld when tomorrow's were pending.
Rebooting after 13:00 while tomorrow is unavailable fetched today's prices successfully, showed them in the UI, and then withheld them from MQTT until the next price point — the #1252 symptom. The tomorrow fetch publishes again if it succeeds, so the extra condition only ever cost correctness.
4. Partial fixed prices were dropped entirely. The three handlers gated the whole payload on
ps->hasPrice(), which only looks at the current price point. A fixed price configured for a period of the year or for certain hours of the day is unknown right now but known later, so nothing at all was published. They now gate onhasAnyPrice(), emitnullfor the points where a dynamic component is involved, and the min/max/cheapest-window scan skips holes instead of stopping at the first one. Home Assistant discovery had the samebreakand now spans holes out to the last point we know a price for, so those entities exist and readunknownuntil their slot arrives.5. Home Assistant price sensors had no expiry margin.
expire_after = resolution * 60 + 300is 3900 s against an hourly publish cadence. One missed publish — an MQTT drop across the hour boundary — took every price entity unavailable for the rest of the hour. They now get a full extra price point of slack.6. Disabling price fetching freed the fixed prices and left a dangling pointer.
handlePriceService()deleted thePriceServicewhenenabled == false, but that object also holds the fixed prices loaded from LittleFS, so following the advice in #1253 to untick the option stopped the fixed price working until the next reboot. It also cleared the pointer inAmsWebServerbut not inEnergyAccounting, whoseif(ps == NULL) return;guard incalcDayCost()therefore did not protect it. The service is now kept alive and reconfigured throughsetup()(which already discards cached dynamic prices and reloads the fixed ones), mirroring what boot does; the teardown path nullsEnergyAccounting's pointer too.getSource()used the same!config->enabledcondition and so reported no source instead ofFIXwhen fetching was enabled but failing.Not fetching what cannot matter
Rather than asking users to untick the box, the firmware now works out whether the dynamic price can affect anything.
getPricePoint()only falls back to the dynamic price where no fixed price applies, so if every hour of today and tomorrow is covered by a fixed price in both directions, fetching is pure cost — a request, an error state and a red banner for a value that is never used.isDynamicPriceNeeded()walks those 48 hours and accumulates which directions a fixed price covers, so a split import/export configuration counts as covered just like a singleBOTHentry. When nothing is needed,loop()skips the fetch, discards any cached containers and clearslastError. The horizon is anchored at the start of today rather than at the current price point, becausecalcDayCost()reaches backwards to midnight; that means it only moves at midnight, so the answer is cached and invalidated on day change and whenever the price configuration is touched. Full 24-hour coverage is also what makes this DST-safe: a config that covers every hour stays covered whichever way the clocks move, and a partial one needs fetching anyway.A partial fixed price keeps fetching, and a period boundary entering the horizon starts fetching a day before it is needed.
This also answers the objection raised in #1253 that users would lose the signal that the price server is down. Where the dynamic price genuinely matters, its absence is still visible: those points publish as
nulland the corresponding Home Assistant entities read unknown. The signal is only suppressed where there is nothing to signal, because no price depends on the server.Also fixed
getFixedPrice()took anint8_t pointwhilegetPricePoint()passes points up to 191 for a 15-minute two-day horizon, so everything from 128 up truncated to a negative offset and a partial fixed price resolved against a time before today's midnight for the back half of tomorrow. Widened touint8_t.Behaviour after this change
nullelsewhere, keeps fetchingVerification
pio run— all six targets indefault_envsbuild (esp8266dev, esp32dev, esp32s2dev, esp32solodev, esp32c3dev, esp32s3dev).pio test -e native— 17/17 decoder tests pass.EEPROM_CHECK_SUMis untouched.Worth a look on real hardware for the two transitions, which I could not exercise here: making a config fully fixed while dynamic prices are cached (containers are discarded, so the price point grid moves from 15 to 60 minutes and Home Assistant re-discovers), and a period boundary entering the horizon.
Known limitation, not addressed here
getResolutionInMinutes()returns 60 whenevertoday == NULL, so a fixed price is published on an hourly grid even in a 15-minute area, and Home Assistant sensors discovered before the first successful fetch carry hour-based display names. Changing the fallback toconfig->resolutionInMinuteswould fix the naming but would also discover 192 entities for a price that never changes, and would need a null guard added ingetPriceForRelativeHour(), which dereferencestodayin its non-60 branch. Left alone; the values are unaffected, asprices.import[n]is always n price points from now.Unrelated and left alone:
getCurrentPrice()has an unusedtslocal that the compiler already warns about.🤖 Generated with Claude Code