Supporting spot energy pricing

Hi All,

OH 4.2 includes a binding for Amber, who are an Australian energy retailer who provide wholesale access to the energy market.

The current binding shows the current pricing that is available, but the Amber API provides a future outlook at 5 or 30min intervals, sample data like below:

[
  {
    "type": "ActualInterval",
    "duration": 5,
    "spotPerKwh": 6.12,
    "perKwh": 24.33,
    "date": "2021-05-05",
    "nemTime": "2021-05-06T12:30:00+10:00",
    "startTime": "2021-05-05T02:00:01Z",
    "endTime": "2021-05-05T02:30:00Z",
    "renewables": 45,
    "channelType": "general",
    "tariffInformation": "string",
    "spikeStatus": "none",
    "descriptor": "negative"
  },
  {
    "type": "CurrentInterval",
    "duration": 5,
    "spotPerKwh": 6.12,
    "perKwh": 24.33,
    "date": "2021-05-05",
    "nemTime": "2021-05-06T12:30:00+10:00",
    "startTime": "2021-05-05T02:00:01Z",
    "endTime": "2021-05-05T02:30:00Z",
    "renewables": 45,
    "channelType": "general",
    "tariffInformation": "string",
    "spikeStatus": "none",
    "descriptor": "negative",
    "range": "string",
    "estimate": true,
    "advancedPrice": "string"
  },
  {
    "type": "ForecastInterval",
    "duration": 5,
    "spotPerKwh": 6.12,
    "perKwh": 24.33,
    "date": "2021-05-05",
    "nemTime": "2021-05-06T12:30:00+10:00",
    "startTime": "2021-05-05T02:00:01Z",
    "endTime": "2021-05-05T02:30:00Z",
    "renewables": 45,
    "channelType": "general",
    "tariffInformation": "string",
    "spikeStatus": "none",
    "descriptor": "negative",
    "range": "string",
    "advancedPrice": "string"
  }
]

Is there a preferred way of loading the future data and making it available in a meaningful manner, so that automations can be driven based on lowest pricing?

Thanks,

Paul.

The short answer is time series (on a phone, sorry). You could have a look at Energi Data Service binding.

Thanks Jacob, I’ll take a look at the Energi Data Service Binding:)

Sorry to resurrect this - but any simple examples of formatting json to suit a time series?

Any good/simple examples of creating a Time Series?

I now have forecast data being read in by the Amber binding, so would like to create a timeseries.

I use a rule to do this, so first a http api call to the provider and than I read 24 values incl their DateTime value out of the json result. With a Persist call you save the values as forecast.

I now have code that takes the raw json and converts the zulu time into the local timezone, and also reads the forecasts:

redicted next interval 2025-05-25T16:05:01+09:30[Australia/Adelaide]-2025-05-25T16:10+09:30[Australia/Adelaide] = 10.79099, 13.59604, 15.76509

The two times are the start and end of the pricing validity - and the 3x values are the low / predicted / high values for the next forecast period. The AU market is dynamic - pricing changes every 5 minutes :slight_smile: Amber provides a forecast for the next 24 x 5 minute periods, then 30min forecasts after that.

openhab-addons/bundles/org.openhab.binding.meteofrance/src/main/java/org/openhab/binding/meteofrance/internal/handler/RainForecastHandler.java at 3607758dea5ac593ebb4b739f4ed4bb7b090666b · psmedley/openhab-addons · GitHub looks useful as a simple example… I’m just going to have to refactor some of my code…

If I understand correctly, I can push the time series to the current channel - I guess it just shows the 1st value as the current price?

OK, I think I have this working… https://github.com/psmedley/openhab-addons/tree/amberupdates has the current code.

Right now, I’m only saving the forecasts for the electricity price, I should add support for Feed-In Tariff.

I also need to add support for configuring how many forecast intervals to fetch - Amber allows this to be configured from 0 to 2048. Logically, next 24 hours makes sense to me - which is 2 hours of 5 min forecasts (24) + 22 hours of 30min forecasts (44) for a total of 68 (if my math is correct!)

Hi @Paul_Smedley - are you sure about that link? I get a 404 when I try to access it.

I’m trying to be lazy and not reinvent the wheel - I have a Sonnen battery and am thinking I’ll join Amber and use this code to control my battery from OpenHab. Amber won’t use SmartShift any time soon with Sonnen batteries. “No problems” says I, I’ll do it myself!

Hey Simon, that code was the branch I was developing on. It’s now been merged into main so is at openhab-addons/bundles/org.openhab.binding.amberelectric at main · openhab/openhab-addons · GitHub

OH 5.0M3 includes this latest change. If you’re still on OH 4.3 I can compile for you if needed.

btw for Sonnen, looks like there is Sonnen Battery Binding but doesn’t look like it can force discharge…

Ah, fair enough. Thanks also for the DM - I’ve joined from the Commbank offer, had already joined by the time I found this thread. I’ve just upgraded to 4.3.(something) after being on 4.1.1 for about a year probably. I can wait for the Amber binding to catch up.

I have the Sonnen binding and have started playing with it, not up to speed with it yet but it has a whole lot of switches, I’m hoping at least one of them tells the battery to discharge and the other says charge. Seems logical enough.

It looks like HA can charge/discarge a Sonnen Battery - see GitHub - weltmeyer/ha_sonnenbatterie: Homeassistant integration to show many stats of Sonnenbatterie - so if the OH binding doesn’t support it, it shouldn’t be hard to extend it.

Looking at openhab-addons/bundles/org.openhab.binding.sonnen at main · psmedley/openhab-addons · GitHub it currently only support force charging.

The API is at documented at sonnenBatterie API - the command for discharge has very similar syntax to the charge command, so it should be very easy to extend the sonnen binding to do this.

Edited on 3/7:

  1. Sonnen “Smart” API is very “managed”
  • The only way to charge from grid is via time-of-use schedules. “Self-consumption” is just “let PV charge, don’t touch the grid.”
  • Discharge is only “allowed” in self-consumption or auto mode, but not as a direct target wattage to the grid (frustrating!).
  • Most third-party apps (Home Assistant, etc) also hack around this by POSTing JSON to /api/v2/configurations endpoints.

  1. Can’t set discharge rate, only “enable” it
  • No official way to tell the battery “discharge at 2kW until I say stop.”
  • You can force export by toggling to self-consumption/auto and raising your local loads, but not via OpenHAB alone.

  1. Not the best for “Energy Arbitrage”
  • Sonnen’s main market is “set and forget” users, not power nerds who want to time energy arbitrage to spot pricing.
  • Fronius, Victron, or (for those in North America) Enphase, Tesla Powerwall have much more open APIs.

  1. API documentation is minimal
  • Most integration info comes from reverse-engineering.
  • There are a few GitHub projects and forum posts that give useful JSON API endpoints and examples.

  1. If you want “real” grid trading, Sonnen isn’t it
  • If you ever get serious about energy market trading, Sonnen won’t let you issue minute-by-minute setpoints for charge/discharge.
  • For “AI” automation and full market participation, brands that allow direct control are essential.

End of edit


Oivay! That took some serious digging and programming. :sweat_smile:

I’ve just finished implementing a full-featured smart battery charging strategy in OpenHAB using Amber price data. It uses SOC and price thresholds along with timer-based throttling for opportunistic “shoulder” charging.

I haven’t integrated Amber’s forecast prices yet, that can happen when I am able to figure out how to use the version 5 add on, unsure if that possible in OH4.3? But that’s next.

OpenHAB really is astonishing when you throw logic, thresholds, and a little creativity at it.

I remember reading something a while back about posting code in the forums and the right way to do it…but I can’t remember what that is currently and it’s late and I’m going to sleep for a while.

Sitemap additions
Frame item=SonnenRecommendation label="Dad's Energy A.I. says " {
        Text item=SonnenRecommendation visibility=[SonnenRecommendation != "NULL"] 
        Text label="Nothing yet, Dad's A.I. brain needs a minute to wake up" visibility=[SonnenRecommendation == "NULL"]
        Text item=EnergyRuleStatus label="Battery Strategy [%s]" valuecolor=[
            "Charging - Price Low"="green",
            "Opportunistic Charge - Mid Price"="olive",
            "Discharging - Price High"="orange",
            "Auto Mode - Normal Price"="blue",
            "Holding - SOC too low ⚠️ Reduce Usage"="red",
            "Holding - Battery Full"="gray"
        ]
        Switch item=BatteryStrategyOverride label="Manual Override" icon="settings"
    }

    Frame label="⚙️ Smart Shift Thresholds" {
        Text label="💡 Price Thresholds"
        Setpoint item=AmberHighPriceThreshold        minValue=0.5  maxValue=2.0  step=0.05
        Setpoint item=AmberLowPriceThreshold         minValue=0.0  maxValue=0.5  step=0.01
        Setpoint item=AmberMidBandLowerThreshold     minValue=0.1  maxValue=0.4  step=0.01
        Setpoint item=AmberMidBandUpperThreshold     minValue=0.25 maxValue=0.6  step=0.01

        Text label="🔋 SOC Limits"
        Setpoint item=SOC_HighPrice_MinDischarge     minValue=5    maxValue=50   step=1
        Setpoint item=SOC_LowPrice_MaxCharge         minValue=50   maxValue=100  step=1
        Setpoint item=SOC_MidBand_MaxCharge          minValue=50   maxValue=100  step=1
    }

    Frame item=Amber_Electric_Current_Electricity_Price label="Electricity Price is " {  
        Text item=Amber_Electric_Current_Electricity_Price label="Electricity Price" valuecolor=[<=0.35="green",<0.50="orange",>=0.50="red"] labelcolor=[<=0.35="green",<0.50="orange",>=0.50="red"]
        Text item=Amber_Electric_Current_Electricity_Status label="Electricity Price Status" valuecolor=["spike"="red","high"="red","neutral"="orange","low"="green","veryLow"="green","extremelyLow"="green","negative"="blue"] labelcolor=["spike"="red","high"="red","neutral"="orange","low"="green","veryLow"="green","extremelyLow"="green","negative"="blue"]
        Switch item=Amber_Electric_Energy_Price_Spike  label="Spike Status"
        Text item=Amber_Electric_Current_FeedIn_Price label="Feed-In Price" valuecolor=[<=0.05="red",<0.20="orange",>=0.20="green"] labelcolor=[<=0.05="red",<0.20="orange",>=0.20="green"]
        Text item=Amber_Electric_Current_FeedIn_Status label="Feed-In Price Status" valuecolor=["high"="green","neutral"="orange","low"="red"] labelcolor=["high"="green","neutral"="orange","low"="red"]
        Text item=Amber_Electric_Current_Controlled_Load_Status label="Controlled Load Price Status"
        Text item=Amber_Electric_Current_Controlled_Load_Price label="Controlled Load Price"
        Text item=Amber_Electric_Current_Renewables label="Renewables Level"
    }
    Frame item=Sonnen_Battery_Battery_Level label="Sonnen Battery capacity is " {  
        Switch item=BatteryStrategyOverride label="Manual Override" icon="settings"
        Default item=Sonnen_Battery_Battery_Level
        Default item=Sonnen_Battery_Grid_Consumption label="Current Grid Consumption"
        Default item=Sonnen_Battery_Consumption label="Current Total House Consumption"
        Text item=Sonnen_Battery_Battery_Discharging
        Switch item=Sonnen_Battery_Battery_Charging_State labelcolor=[ON="green",OFF="red"]
        Switch item=Sonnen_Battery_Battery_Discharging_State labelcolor=[ON="green",OFF="red"]
        Default item=Sonnen_Battery_Battery_Charging
        Switch item=Sonnen_Battery_StartStop_Active_Battery_Charging_from_Grid
        Default item=Sonnen_Battery_Flow_Battery_Towards_Consumption_State
        Default item=Sonnen_Battery_Flow_Grid_Towards_Consumption_State
        Default item=Sonnen_Battery_Flow_Production_Towards_Consumption_State
        Default item=Sonnen_Battery_Flow_Grid_Towards_Battery_State
        Default item=Sonnen_Battery_Flow_Production_Towards_Battery_State
        Default item=Sonnen_Battery_Flow_Production_Towards_Grid_State
        Default item=Sonnen_Battery_Imported_kWh_Production
        Default item=Sonnen_Battery_Exported_kWh_Production
        Default item=Sonnen_Battery_Imported_kWh_Consumption
        Default item=Sonnen_Battery_Exported_kWh_Consumption
        Default item=Sonnen_Battery_Battery_Operation_Mode
        Default item=Sonnen_Battery_Grid_Feed_In label="Current Feed In to the Grid"
        Default item=Sonnen_Battery_Solar_Production
    }
Manually created switches (in SonnenThings.items)
Switch BatteryChargingFromGrid "Charge Battery from Grid" { channel="sonnen:sonnenbattery:fb608b3d17:batteryChargingFromGrid" }
Number SonnenSOC "Battery SOC [%d %%]" { channel="sonnen:sonnenbattery:fb608b3d17:batteryLevel" }
String EnergyRuleStatus "Battery Strategy Status [%s]" <status>
Switch BatteryStrategyOverride "Override Energy Strategy" <settings>
DateTime AmberPriceLastUpdate "Last Price Update [%1$tF %1$tT]"
DateTime SonnenSOC_LastUpdate
DateTime PVProduction_LastUpdate
Switch OpportunisticChargeTimerRunning "Opportunistic Charging Active"
Number OpportunisticChargeCycleCount
Switch OpportunisticChargeActive
Switch RunSonnenModeTest "Run Sonnen Mode Test"
Switch RunSonnenTimeOfUseTest "Run Sonnen Mode Test"
Switch RunSonnenManualTest "Run Sonnen Manual Test"
Switch RunSonnenApiTest "Run Sonnen API Test"
String Amber_Electric_Current_Electricity_Price_LastUpdate "Last Price Update" <clock>
String Amber_Electric_Feed_In_Price_LastUpdate "Last Price Update" <clock>
String Dad_AI_Recommendation "Dad's AI Recommendation"

// --- Adjustable Thresholds ---
Number AmberHighPriceThreshold "High Price Threshold [%.2f]"         // e.g., 0.70
Number AmberLowPriceThreshold "Low Price Threshold [%.2f]"           // e.g., 0.20
Number AmberMidBandUpperThreshold "Mid Band Upper Threshold [%.2f]"  // e.g., 0.35
Number AmberMidBandLowerThreshold "Mid Band Lower Threshold [%.2f]"  // e.g., 0.20

// --- SOC Thresholds ---
Number SOC_HighPrice_MinDischarge "Min SOC to Discharge [%d %%]"     // e.g., 10
Number SOC_LowPrice_MaxCharge     "Max SOC to Charge [%d %%]"        // e.g., 98
Number SOC_MidBand_MaxCharge      "Max SOC Mid Band [%d %%]"         // e.g., 100

// --- Proxy Amber representations of the Thresholds for use in the Main UI
Number AmberHighPriceThreshold_Proxy "High Price Threshold Proxy [$%.2f]" 
Number AmberLowPriceThreshold_Proxy  "Low Price Threshold Proxy [$%.2f]"
Number AmberMidBandLowerThreshold_Proxy "Mid Band Lower Proxy [$%.2f]"
Number AmberMidBandUpperThreshold_Proxy "Mid Band Upper Proxy [$%.2f]"

// --- Proxy SOC representations of the Thresholds for use in the Main UI
Number SOC_HighPrice_MinDischarge_Proxy "Min SOC to Discharge [%d %%]"     
Number SOC_LowPrice_MaxCharge_Proxy     "Max SOC to Charge [%d %%]"        
Number SOC_MidBand_MaxCharge_Proxy      "Max SOC Mid Band [%d %%]"
My 'AmberSonnenSmartShiftCharging.rules'...rules
import java.time.Duration

// Sonnen API call helper lambda
val sendSonnenAPI = [ String jsonPayload |
    val logger = "SonnenTest"
    val output = executeCommandLine(
        Duration.ofSeconds(10),
        "/usr/bin/curl",
        "-s",
        "-X", "PUT",
        "--header", "Content-Type: application/json",
        "--header", "Auth-Token: xxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
        "-d", jsonPayload,
        "http://xxxxxxxxxxxxxxxxxxx/api/v2/configurations"
    )
    logInfo(logger, "🚦 API PUT: " + jsonPayload)
    logInfo(logger, "Sonnen API response: " + output)
]

// --- Main Rule for Battery Control ---
rule "Electricity Price Control with SOC - Detailed Bands (Sonnen API)"
when
    Time cron "0 0/15 * * * ?" or
    Item Amber_Electric_Current_Electricity_Price changed
then
    // --- Manual Override Check ---
    if (BatteryStrategyOverride.state === NULL) {
        logWarn("Amber", "BatteryStrategyOverride is NULL — setting default OFF")
        BatteryStrategyOverride.postUpdate(OFF)
    } else if (BatteryStrategyOverride.state == ON) {
        logWarn("Amber", "⚙️ Manual Override ENABLED - skipping rule logic.")
        SonnenRecommendation.postUpdate("⚠️ Manual Override Enabled - no automatic battery control.")
        EnergyRuleStatus.postUpdate("⚠️ Manual Override")
        return;
    }

    // --- Price Data Check ---
    val updateState = AmberPriceLastUpdate.state
    if (updateState === NULL || updateState == UNDEF) {
        logWarn("Amber", "⚠️ AmberPriceLastUpdate is NULL or UNDEF — assuming stale data")
        SonnenRecommendation.postUpdate("⚠️ Skipped logic: No recent price update")
        EnergyRuleStatus.postUpdate("⚠️ Stale Data - No Update Timestamp")
        return;
    }

    val lastUpdate = updateState as DateTimeType
    val minutesSinceUpdate = java.time.Duration.between(lastUpdate.getInstant(), now.toInstant).toMinutes
    if (minutesSinceUpdate > 20) {
        logWarn("Amber", "⚠️ Price data is stale ({} min) — skipping control", minutesSinceUpdate)
        SonnenRecommendation.postUpdate("⚠️ Skipped logic: Amber price data too old")
        EnergyRuleStatus.postUpdate("⚠️ Stale Data - Holding")
        return;
    }

    // --- State Checks ---
    val priceState = Amber_Electric_Current_Electricity_Price.state
    val socState = SonnenSOC.state

    if (priceState === NULL || priceState == UNDEF || socState === NULL || socState == UNDEF) {
        logWarn("Amber", "Price or SOC is NULL/UNDEF — aborting rule.")
        SonnenRecommendation.postUpdate("⚠️ Skipped: Missing critical data")
        EnergyRuleStatus.postUpdate("⚠️ Incomplete Data")
        return;
    }

    // --- Dynamic Threshold Fetching ---
    val highPriceThreshold = if (AmberHighPriceThreshold.state !== NULL && AmberHighPriceThreshold.state != UNDEF)
        (AmberHighPriceThreshold.state as Number).doubleValue
    else {
        AmberHighPriceThreshold.postUpdate(0.75)
        0.75
    }
    val lowPriceThreshold = if (AmberLowPriceThreshold.state !== NULL && AmberLowPriceThreshold.state != UNDEF)
        (AmberLowPriceThreshold.state as Number).doubleValue
    else {
        AmberLowPriceThreshold.postUpdate(0.20)
        0.20
    }
    val midBandUpper = if (AmberMidBandUpperThreshold.state !== NULL && AmberMidBandUpperThreshold.state != UNDEF)
        (AmberMidBandUpperThreshold.state as Number).doubleValue
    else {
        AmberMidBandUpperThreshold.postUpdate(0.35)
        0.35
    }
    val midBandLower = if (AmberMidBandLowerThreshold.state !== NULL && AmberMidBandLowerThreshold.state != UNDEF)
        (AmberMidBandLowerThreshold.state as Number).doubleValue
    else {
        AmberMidBandLowerThreshold.postUpdate(0.20)
        0.20
    }
    val socHighMin = if (SOC_HighPrice_MinDischarge.state !== NULL && SOC_HighPrice_MinDischarge.state != UNDEF)
        (SOC_HighPrice_MinDischarge.state as Number).intValue
    else {
        SOC_HighPrice_MinDischarge.postUpdate(10)
        10
    }
    val socLowMax = if (SOC_LowPrice_MaxCharge.state !== NULL && SOC_LowPrice_MaxCharge.state != UNDEF)
        (SOC_LowPrice_MaxCharge.state as Number).intValue
    else {
        SOC_LowPrice_MaxCharge.postUpdate(100)
        100
    }
    val socMidMax = if (SOC_MidBand_MaxCharge.state !== NULL && SOC_MidBand_MaxCharge.state != UNDEF)
        (SOC_MidBand_MaxCharge.state as Number).intValue
    else {
        SOC_MidBand_MaxCharge.postUpdate(91)
        91
    }

    val currentPrice = (priceState as QuantityType<Number>).toUnit("AUD/kWh").doubleValue
    val soc = (socState as Number).doubleValue
    val roundedPrice = Math::round(currentPrice * 10000.0) / 10000.0
    val socFormatted = String::format("%.1f", soc)
    val gridState = if (Sonnen_Battery_Grid_Consumption.state !== NULL && Sonnen_Battery_Grid_Consumption.state != UNDEF)
        Sonnen_Battery_Grid_Consumption.state.toString else "unknown"
    val dischargeState = if (Sonnen_Battery_Battery_Discharging.state !== NULL && Sonnen_Battery_Battery_Discharging.state != UNDEF)
        Sonnen_Battery_Battery_Discharging.state.toString else "unknown"

    val energyinfosummary = "($" + roundedPrice + " p/kWh, SOC: " + socFormatted + "%, GRID: " + gridState + ", BATT: " + dischargeState + ")"

    logInfo("Amber", "🔍 " + energyinfosummary)
    logInfo("Amber", "📊 Thresholds — High: {}, Low: {}, Mid: {}-{}, SOC HighMin: {}, SOC LowMax: {}, SOC MidMax: {}",
        highPriceThreshold, lowPriceThreshold, midBandLower, midBandUpper, socHighMin, socLowMax, socMidMax
    )

    // ⚡️ --- High Price Band (DISCHARGE/AUTO) ---
    if (currentPrice >= highPriceThreshold) {
        logInfo("Amber", "⚠️ High price: Discharging allowed" + energyinfosummary)
        val json = '{"EM_OperatingMode":"2"}'
        sendSonnenAPI.apply(json)
        EnergyRuleStatus.postUpdate("Discharging - Price High")
        SonnenRecommendation.postUpdate("⚠️ High price: Discharging allowed " + energyinfosummary)
        Dad_AI_Recommendation.postUpdate("⚠️ High price: Discharging the battery if possible: $" + roundedPrice + "/kWh, SOC: " + socFormatted + "%")
        val actions = getActions("pushover", "pushover:pushover-account:xxxxxxxxxxxxxxxxxxxx")
                    var message = "⚠️ High Electricity price! $" + roundedPrice + "/kWh, SOC: " + socFormatted + "%"
                    if (YouveBeenWarned.state != ON) {
                        actions.sendMessage("⚠️ High Electricity price!", message)
                    }
        if (soc <=10) {
            logInfo("Amber", "⚠️ High price: No battery power available! Shift energy conumption if possible" + energyinfosummary)
            SonnenRecommendation.postUpdate("⚠️ High price: No battery power available! Shift energy conumption if possible " + energyinfosummary)
            Dad_AI_Recommendation.postUpdate("⚠️ High price: No battery power available! Shift energy conumption if possible $" + roundedPrice + "/kWh, SOC: " + socFormatted + "%")
            val actions1 = getActions("pushover", "pushover:pushover-account:xxxxxxxxxxxxxxxx")
            var message1 = "⚠️ High Electricity price but no battery power available! $" + roundedPrice + "/kWh, SOC: " + socFormatted + "%"
            actions1.sendMessage("⚠️ High Electricity price! No battery power available!", message1)        
        }
        YouveBeenWarned.sendCommand("ON")
    // 🔋 --- Low Price Band (CHARGE with ToU) ---
    } else if (currentPrice <= lowPriceThreshold) {
        logInfo("Amber", "🔋 Low Price: Charging battery from grid" + energyinfosummary)
        val startTime = String::format("%02d:%02d", now.getHour, now.getMinute)
        val endDT = now.plusMinutes(10)
        val friendlyTime = String::format("%02d:%02d", endDT.getHour, endDT.getMinute)
        val endTime = String::format("%02d:%02d", endDT.getHour, endDT.getMinute)
        val scheduleJson = '[{\\"start\\":\\"' + startTime + '\\",\\"stop\\":\\"' + endTime + '\\",\\"threshold_p_max\\":20000}]'
        val json = '{"EM_OperatingMode":"10","EM_ToU_Schedule":"' + scheduleJson + '"}'
        sendSonnenAPI.apply(json)
        EnergyRuleStatus.postUpdate("Charging - Price Low")
        SonnenRecommendation.postUpdate("🔋 Charging battery from grid " + energyinfosummary)
        Dad_AI_Recommendation.postUpdate("🔋 Charging battery from grid until " + friendlyTime + ": $" + roundedPrice + "/kWh, SOC: " + socFormatted + "%")

    // 🌤 --- Mid Band (opportunistic ToU charge) ---
    } else if (currentPrice > midBandLower && currentPrice <= midBandUpper) {
        if (soc < socMidMax) {
            val startTime = String::format("%02d:%02d", now.getHour, now.getMinute)
            val endDT = now.plusMinutes(8)
            val friendlyTime = String::format("%02d:%02d", endDT.getHour, endDT.getMinute)
            val endTime = String::format("%02d:%02d", endDT.getHour, endDT.getMinute)
            val scheduleJson = '[{\\"start\\":\\"' + startTime + '\\",\\"stop\\":\\"' + endTime + '\\",\\"threshold_p_max\\":4000}]'
            val json = '{"EM_OperatingMode":"10","EM_ToU_Schedule":"' + scheduleJson + '"}'
            sendSonnenAPI.apply(json)
            logInfo("Amber", "🌤️ Mid band: Opportunistic ToU charge for 8 min " + energyinfosummary)
            EnergyRuleStatus.postUpdate("Opportunistic Charge - Mid Price (Throttled)")
            SonnenRecommendation.postUpdate("🌤️ Throttled Charging battery " + energyinfosummary)
            Dad_AI_Recommendation.postUpdate("🌤️ Throttled Charging battery until " + friendlyTime + ": $" + roundedPrice + "/kWh, SOC: " + socFormatted + "%")
        } else {
            val json = '{"EM_OperatingMode":"2"}'
            sendSonnenAPI.apply(json)
            OpportunisticChargeTimerRunning.sendCommand(OFF)
            EnergyRuleStatus.postUpdate("Holding - Battery Full")
            SonnenRecommendation.postUpdate("⚙️ Cheap price, but battery full, setting to Auto Mode " + energyinfosummary)
            Dad_AI_Recommendation.postUpdate("⚙️ Cheap price, but battery full, setting to Auto Mode: $" + roundedPrice + "/kWh, SOC: " + socFormatted + "%")
        }

    // ⚙️ --- Normal Band (Auto Mode) ---
    } else {
        logInfo("Amber", "⚙️ Normal price: Battery in auto mode " + energyinfosummary)
        val json = '{"EM_OperatingMode":"2"}'
        sendSonnenAPI.apply(json)
        EnergyRuleStatus.postUpdate("Auto Mode - Normal Price")
        SonnenRecommendation.postUpdate("⚙️ Normal price: Battery in auto mode " + energyinfosummary)
        Dad_AI_Recommendation.postUpdate("⚙️ Normal price: Battery in auto mode: $" + roundedPrice + "/kWh, SOC: " + socFormatted + "%")
    }
end

// --- Debug/Diagnostics and Timestamp Tracking ---

rule "Debug AmberPriceLastUpdate"
when
    Item AmberPriceLastUpdate changed
then
    logWarn("Amber", "AmberPriceLastUpdate changed to " + AmberPriceLastUpdate.state.toString)
end

rule "Track Amber Price Updates"
when
    Item Amber_Electric_Current_Electricity_Price changed
then
    AmberPriceLastUpdate.postUpdate(now.toString)
end

I’ve also added some startup rules that check if the variables are NULL or Undefined and corrects those on system start up. I can provide that if anyone is interested.

Possible Future Enhancements:

  • :crystal_ball: Amber Forecast Integration: Predictive scheduling based on forward price signals.
  • :brain: Adaptive SOC Targets: Adjust socLowMax or socMidMax based on time of day, season, or predicted consumption ← Done, this is now embedded in the rule posted above.
  • :cloud: Solar Forecast Awareness: Delay charging if tomorrow is expected to be sunny.
  • :bar_chart: InfluxDB / Grafana: For tracking trends in charging behaviour vs. price vs. SOC.

Way OT for this thread, but (untested) code to force discharge your Sonnen is in the Sonnen thread at Sonnen Battery Binding - #39 by Paul_Smedley

Ooh! Oh…hangon…that makes most of my experiments sort seem…well…like wheel re-inventing. WELL SHUCKS THANKS PAAAAUL!

No seriously, thank you! I will probably need to update to 5.0.0 milestone release I guess. That’s also where the Amber goodness comes in I think. Spot energy trading here I come!!

I don’t have an own Sonnen battery to tinker with, but I’m looking to use one in an energy management system that can do arbitrage trading and the like.

So from what I understood so far, the sonnen binding and with it the code to charge/discharge the battery is part of current OH 5.1, is that right?

Now to be frank I did not understand how to use that. The charge/discharge channels are read-only and then there’s a RW switch batteryChargingFromGrid.
How do I tell the battery to charge with a specific amount of power and how do I tell it to stop?
And the same question for forced discharging to grid, how do I make use of that?
Thank you for enlightening me…

From memory, the current Sonnen code can charge from grid (using batteryChargingFromGrid at a set rate) only. I released a build that should support discharge using the same methodology, but nobody tested it AFAIK, so the PR is just sitting in my repo.

GitHub - psmedley/openhab-addons at sonnenforcecharge is anyone is interested

Thanks. So as I cannot set the rate, how do I tell it to discharge?
Is there another Switch type channel batterDischargingToGrid or the like?