From c1f8f6ad13e53538535aa2d0d181342f66edbd6e Mon Sep 17 00:00:00 2001 From: Raman Gupta Date: Fri, 30 Oct 2026 18:07:11 -0400 Subject: [PATCH] feat: send summaryCostFromItemizedCharges quirk for Burlington Hydro Burlington bills Halton Region water every other month on the electricity bill with no line item for it, and billLastPeriod now carries that whole-bill total. The new quirk tells the HA integration to price each bill from its itemized charges. Regenerated native-image metadata for the new UtilityQuirks constructor parameter. --- docs/utilities/burlington-hydro.md | 9 +++++++++ .../kotlin/org/opengb/utility/UtilityProfile.kt | 13 +++++++++++++ .../from-app/reachability-metadata.json | 1 + .../from-tests/reachability-metadata.json | 1 + server/app/src/main/resources/utilities.conf | 6 ++++++ .../org/opengb/utility/UtilityQuirksConfigTest.kt | 15 +++++++++++++++ 6 files changed, 45 insertions(+) diff --git a/docs/utilities/burlington-hydro.md b/docs/utilities/burlington-hydro.md index deea4af..7882819 100644 --- a/docs/utilities/burlington-hydro.md +++ b/docs/utilities/burlington-hydro.md @@ -23,3 +23,12 @@ Only electricity data is available for Burlington Hydro through Open Green Butto See the [London Hydro provider notes](../providers/london-hydro.md). Requesting the water function block (FB_11) at Burlington's authorize endpoint is rejected with `invalid_scope` (tested 2026-10-09), so do not add it to this utility's `defaultScope`. + +## Bill Cost + +Every other bill also charges two months of Halton Region water, and the feed folds that into the bill's total without a line item for it. +On those bills `billLastPeriod`, "New Charges This Period" and "Total Amount Due" are the whole bill, for example $502.29 on the June 19 2026 bill, of which $314.56 was water (confirmed against the PDF bill, 2026-10-30). +The itemized lines (time-of-use, delivery, regulatory, H.S.T. and the Ontario Electricity Rebate) add up to the electricity alone, and the H.S.T. line is 13% of those charges only. + +So Burlington sets the `summaryCostFromItemizedCharges` quirk, and the Home Assistant integration prices each bill from its itemized charges rather than its total. +Other utilities operated by London Hydro that bill water may need the same quirk once checked against a bill. diff --git a/server/app/src/main/kotlin/org/opengb/utility/UtilityProfile.kt b/server/app/src/main/kotlin/org/opengb/utility/UtilityProfile.kt index 51d3c55..a0c301c 100644 --- a/server/app/src/main/kotlin/org/opengb/utility/UtilityProfile.kt +++ b/server/app/src/main/kotlin/org/opengb/utility/UtilityProfile.kt @@ -225,6 +225,15 @@ data class UtilityQuirks( * usage is read. */ val enbridgeMeterReads: Boolean = false, + /** + * A UsageSummary's `billLastPeriod` (and its "New Charges This Period" / "Total Amount Due" + * lines) is the whole bill, including charges for services that aren't this usage point's, so + * the client prices each bill from its itemized charge lines instead. Burlington Hydro bills + * Halton Region water every other month on the same bill: a $187.73 electricity bill arrives + * with `billLastPeriod` = $502.29 and no line item for the $314.56 of water. Nothing in the feed + * marks those months; only the H.S.T. line, 13% of the electricity charges alone, gives it away. + */ + val summaryCostFromItemizedCharges: Boolean = false, ) { /** Quirks the HA client has to apply itself (it parses the ESPI), as `OpenGB-Quirks` tokens. */ val clientQuirks: List @@ -234,6 +243,7 @@ data class UtilityQuirks( CLIENT_QUIRK_INVERT_COST_IF_POSITIVE_MULTIPLIER.takeIf { invertCostIfPositiveMultiplier }, CLIENT_QUIRK_INTERVAL_COST_IN_THOUSANDTHS.takeIf { intervalCostInThousandths }, CLIENT_QUIRK_ENBRIDGE_METER_READS.takeIf { enbridgeMeterReads }, + CLIENT_QUIRK_SUMMARY_COST_FROM_ITEMIZED_CHARGES.takeIf { summaryCostFromItemizedCharges }, ) } @@ -249,6 +259,9 @@ const val CLIENT_QUIRK_INTERVAL_COST_IN_THOUSANDTHS: String = "intervalCostInTho /** `OpenGB-Quirks` token for [UtilityQuirks.enbridgeMeterReads]. */ const val CLIENT_QUIRK_ENBRIDGE_METER_READS: String = "enbridgeMeterReads" +/** `OpenGB-Quirks` token for [UtilityQuirks.summaryCostFromItemizedCharges]. */ +const val CLIENT_QUIRK_SUMMARY_COST_FROM_ITEMIZED_CHARGES: String = "summaryCostFromItemizedCharges" + /** Wire format for ESPI date-filter values. See [UtilityQuirks.dateFilterFormat]. */ enum class DateFilterFormat { /** ISO 8601 instant, e.g. `2024-08-06T20:19:33Z` — the ESPI spec form. */ diff --git a/server/app/src/main/resources/META-INF/native-image/org.opengb/from-app/reachability-metadata.json b/server/app/src/main/resources/META-INF/native-image/org.opengb/from-app/reachability-metadata.json index 2367a7d..9bb33a5 100644 --- a/server/app/src/main/resources/META-INF/native-image/org.opengb/from-app/reachability-metadata.json +++ b/server/app/src/main/resources/META-INF/native-image/org.opengb/from-app/reachability-metadata.json @@ -2501,6 +2501,7 @@ "boolean", "boolean", "boolean", + "boolean", "int", "kotlin.jvm.internal.DefaultConstructorMarker" ] diff --git a/server/app/src/main/resources/META-INF/native-image/org.opengb/from-tests/reachability-metadata.json b/server/app/src/main/resources/META-INF/native-image/org.opengb/from-tests/reachability-metadata.json index b91af7a..adc7c99 100644 --- a/server/app/src/main/resources/META-INF/native-image/org.opengb/from-tests/reachability-metadata.json +++ b/server/app/src/main/resources/META-INF/native-image/org.opengb/from-tests/reachability-metadata.json @@ -2941,6 +2941,7 @@ "boolean", "boolean", "boolean", + "boolean", "int", "kotlin.jvm.internal.DefaultConstructorMarker" ] diff --git a/server/app/src/main/resources/utilities.conf b/server/app/src/main/resources/utilities.conf index 850be71..b8b5529 100644 --- a/server/app/src/main/resources/utilities.conf +++ b/server/app/src/main/resources/utilities.conf @@ -126,6 +126,12 @@ utilities = [ # commitment made to Burlington. A correctly-scoped published-min probe, once run with a # healthy (non-poisoned) cursor, returned exactly the newest day of data as expected — see # docs/utilities/burlington-incremental-issue.md for the full investigation. + # Every other bill also carries two months of Halton Region water in its total, with no line + # item for it, so cost comes from the itemized electricity charges (confirmed 2026-10-30 + # against the June 19 2026 bill: $314.56 of water on a $502.29 total). + quirks { + summaryCostFromItemizedCharges = true + } } # Consumers Energy diff --git a/server/app/src/test/kotlin/org/opengb/utility/UtilityQuirksConfigTest.kt b/server/app/src/test/kotlin/org/opengb/utility/UtilityQuirksConfigTest.kt index 929f017..0691c00 100644 --- a/server/app/src/test/kotlin/org/opengb/utility/UtilityQuirksConfigTest.kt +++ b/server/app/src/test/kotlin/org/opengb/utility/UtilityQuirksConfigTest.kt @@ -52,6 +52,21 @@ val UtilityQuirksConfigTest by testSuite { assert(on == listOf("enbridge")) { "$on" } } + test("summaryCostFromItemizedCharges defaults off and adds only its own client quirk") { + assert(!loadHocon("{}").quirks.summaryCostFromItemizedCharges) + val quirks = loadHocon("quirks: { summaryCostFromItemizedCharges: true }").quirks + assert(quirks.clientQuirks == listOf(CLIENT_QUIRK_SUMMARY_COST_FROM_ITEMIZED_CHARGES)) { + "got ${quirks.clientQuirks}" + } + } + + test("the packaged utilities.conf prices bills from itemized charges for Burlington only") { + val utilities = ConfigFactory.parseResources("utilities.conf").getConfigList("utilities") + val path = "quirks.summaryCostFromItemizedCharges" + val on = utilities.filter { it.hasPath(path) && it.getBoolean(path) }.map { it.getString("id") } + assert(on == listOf("burlington_hydro")) { "$on" } + } + test("the packaged utilities.conf scales interval cost in thousandths for Alectra only") { val utilities = ConfigFactory.parseResources("utilities.conf").getConfigList("utilities") val path = "quirks.intervalCostInThousandths"