Edited on 3/7:
- 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.
- 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.
- 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.
- 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.
- 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. 
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:
Amber Forecast Integration: Predictive scheduling based on forward price signals.
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.
Solar Forecast Awareness: Delay charging if tomorrow is expected to be sunny.
InfluxDB / Grafana: For tracking trends in charging behaviour vs. price vs. SOC.