Electric Car - Need help creating rules based on logs

Hi Norbert,
This is not a simple rule you want to write.

For the trigger look at: https://www.openhab.org/docs/configuration/rules-dsl.html#rule-triggers

For the energy over time you could look at: Rule for Total energy consumed in kWh for day or Best practice for handling energy consumption in a single cummulative value?

Search the forum there are more…

Have a go at writing your rule, if you get stuck, post your code and we’ll help. We will not write your code for you. You ask for a helping hand.

sure, current translates into the same…

hm…going simple steps…i would have to check with every update if now POWER is >1000W. I would have a variable named ChargeState that goes True when this happens…if boolean exists?

And it goes False if it was true and power suddenly is Zero again.

For the delivered energy part i’m more unsure, do i really need a StartEnergy variable that is stored when the True is set…and somehow subtract from the EnergyImp-State when finished…I guess so/or it needs to be like that if not a more simple way is possible. I do not want to have too many variables that nobody needs except for these calculations…Is there any of these “since…” calculations possible that are done automatically?

Kind Regards

As already suggested, look at what rule triggers are available.
when X > NN is not.
You can simulate with
when X changed
then
if (X > NN)

All rather depends what data you get from your charger.
I’d be investigating if this kind of thing was practical
when chargeCurrent changed from 0

In usual openHAB terms, you might have a Switch type Item called “charging” and turn it on and off.

Sure, why not.

They cost you nothing. If you store important data in Items you can exploit other openHAB features like for example persistence, so that they can survive a system reboot.

Just doing the first detection part right now, most of my rules are done with “received update”. you suggest “changed”. e.g. Item EVchargePower received update vs. changed

From what i understand “received update” would trigger even if the value stays the same, and changed will only be triggered if the value itself really changed…so if the new value is the same as the old, the rule would not be triggered.

Is that true?
because i would guess that received update is more safe in that it would not take into consideration that the value itself needs to be different…

OnTOP: one more question:
if the energy_previous has to be stored…is this declared within the rule itself or on a global level…or does local/global not exist for rules…so wherever you define them, they are available everywhere?

Please have a short critical view on my first part, does it look OK/efficient/stable…thanks!

Switch Car_Charging "CarCharging"
Number EVchargeMeter "EVcharge Meter"

rule "ChargeStart Detection"
when
    Item EVchargePower received update
then
    if ((EVchargePower > 500) && Car_Charging.state == OFF &&) 
	   { Car_Charging.sendCommand(ON)
	     EVchargeMeter.postUpdate(EVchargeWhImp.state)
		}
end

This is what I came up with for gathering charging data for one of our EVs. This uses the new rule engine, Jython and the helper libraries. The outlet that the car plugs into (just 120V) is also used to charge batteries for the lawn mower, weed eater, chainsaw, etc., so there’s logic built in to detect what is charging.

We have Alexa enabled devices and the OH skill. To start a charge, we just say something like, ‘Echo, set Full Charge Mileage to 5.5’. If the car was unplugged but not completely charged, then ‘Echo, set Partial Charge Mileage to 55.5’. I have notifications setup, so after a full charge mileage is set, you will get the stats of the last drive and also when charging (car or battery) is complete.

Hope this helps!

ITEMS
I haven’t gotten everything migrated to UoM yet :blush:

Number:Length  Volt_Mileage                "Full Charge Mileage [%.1f mi]"                <text>              (gBattery_Charging)    {alexa="RangeController.rangeValue" [supportedRange="0:500:1"]}
Number:Length  Volt_Mileage_Partial        "Partial Charge Mileage [%.1f mi]"             <text>              (gBattery_Charging)    {alexa="RangeController.rangeValue" [supportedRange="0:500:1"]}
Number:Length  Volt_Mileage_Total          "Volt Mileage (total) [%.1f mi]"               <text>              (gBattery_Charging)
Number:Energy  Volt_Energy                 "Volt Energy (last charge) [%.0f kWh]"          <energy>            (gBattery_Charging)
Number:Energy  Volt_Energy_Total           "Volt Energy (total) [%.0f kWh]"                <energy>            (gBattery_Charging)
Number  Volt_Efficiency             "Volt Efficiency (last charge) [%.0f Wh/mi]"  <energy>            (gBattery_Charging)
Number  Volt_Efficiency_Average     "Volt Efficiency (average) [%.0f Wh/mi]"      <energy>            (gBattery_Charging)
Number  Volt_Cost                   "Volt Cost (last charge) [$%.2f]"               <text>              (gBattery_Charging)
Number  Volt_Cost_Total             "Volt Cost (total) [$%.2f]"                     <text>              (gBattery_Charging)
Number  Volt_Cost_PerMile           "Volt Cost/Mile (last charge) [$%.3f/mi]"     <text>              (gBattery_Charging)
Number  Volt_Cost_PerMile_Average   "Volt Cost/Mile (average) [$%.3f/mi]"         <text>              (gBattery_Charging)

Switch	Outlet9	                        "Car Charger [%s]"	                                        <poweroutlet-us>	(gGarageAttached,gOutlet,gBattery_Charging)                                             {channel="zwave:device:55555:node225:switch_binary", alexa="PowerController.powerState" [category="SMARTPLUG"]}
Number:Power	Outlet9_Power	                "Car Charger: Power [%.0f W]"	                            <energy>	        (gGarageAttached,gOutlet,gBattery_Charging)		                                        {channel="zwave:device:55555:node225:meter_watts"}
Number:Energy	Outlet9_Energy	                "Car Charger: Energy [%.0f kWh]"	                        <energy>	        (gGarageAttached,gOutlet,gBattery_Charging)		                                        {channel="zwave:device:55555:node225:meter_kwh"}

SCRIPT

from threading import Timer

from core.rules import rule
from core.triggers import when
from core.actions import PersistenceExtensions

from personal.utils import notification

from org.joda.time import DateTime

ENERGY_COST_PER_KWH = 0.1238
CHARGER_TIMER_ATTACHED = None# Outlet9

@rule("Power: Battery charging")
@when("Item Outlet9 changed to OFF")
def battery_charging(event):
    battery_charging.log.debug("Battery charging: {}: start".format(event.itemState))
    date_of_last_full_charge = DateTime(PersistenceExtensions.previousState(ir.getItem("Volt_Mileage"), True).timestamp.time)
    date_of_last_partial_charge = DateTime(PersistenceExtensions.previousState(ir.getItem("Volt_Mileage_Partial")).timestamp.time)
    last_charge_was_full = date_of_last_full_charge.isAfter(date_of_last_partial_charge)

    charge_start = date_of_last_full_charge if last_charge_was_full else date_of_last_partial_charge
    battery_charging.log.debug("Battery charging: charge_start=[{}]".format(charge_start))
    if PersistenceExtensions.maximumSince(ir.getItem("Outlet9_Power"), charge_start).state > DecimalType(900):
        message = "Car charging has completed"
    else:
        message = "Battery charging has completed"
    notification("Battery charging", message)

@rule("Power: Battery charging monitor")
@when("Item Outlet9_Power changed")
def battery_charging_monitor(event):
    #battery_charging_monitor.log.debug("Battery charging monitor: {}: start".format(event.itemState))
    global CHARGER_TIMER_ATTACHED
    if items["Outlet9"] == ON and event.itemState <= QuantityType("8 W") and event.oldItemState <= QuantityType("8 W"):
        if CHARGER_TIMER_ATTACHED is None or str(CHARGER_TIMER_ATTACHED.getState()) == "TERMINATED":
            CHARGER_TIMER_ATTACHED = Timer(600, lambda: events.sendCommand("Outlet9", "OFF"))
            CHARGER_TIMER_ATTACHED.start()
            battery_charging_monitor.log.debug("Battery charging monitor: Started battery charging turn off timer: Outlet9_Power=[{}], oldItemState=[{}]".format(event.itemState, event.oldItemState))
    elif CHARGER_TIMER_ATTACHED is not None and str(CHARGER_TIMER_ATTACHED.getState()) == "TIMED_WAITING":
        CHARGER_TIMER_ATTACHED.stop()
        battery_charging_monitor.log.debug("Battery charging monitor: Cancelled battery charging turn off timer: Outlet9_Power=[{}], oldItemState=[{}]".format(event.itemState, event.oldItemState))

@rule("Power: Volt mileage received update")
@when("Item Volt_Mileage received update")
def volt_mileage(event):
    volt_mileage.log.debug("Volt mileage change: {}: start".format(event.itemState))
    # these calculations are all for the last charge
    date_of_last_full_charge = DateTime(PersistenceExtensions.previousState(ir.getItem("Volt_Mileage"), True).timestamp.time)
    date_of_last_partial_charge = DateTime(PersistenceExtensions.previousState(ir.getItem("Volt_Mileage_Partial")).timestamp.time)
    last_charge_was_full = date_of_last_full_charge.isAfter(date_of_last_partial_charge)

    volt_mileage.log.debug("Volt mileage change: date_of_last_full_charge=[{}], date_of_last_partial_charge=[{}], last_charge_was_full=[{}]".format(date_of_last_full_charge, date_of_last_partial_charge, last_charge_was_full))
    previous_mileage = PersistenceExtensions.previousState(ir.getItem("Volt_Mileage" if last_charge_was_full else "Volt_Mileage_Partial"), True).state.floatValue()
    volt_mileage.log.debug("Volt mileage change: previous_mileage=[{}]".format(previous_mileage))
    total_mileage = items["Volt_Mileage_Total"].floatValue() + previous_mileage
    events.sendCommand("Volt_Mileage_Total", str(total_mileage))
    volt_mileage.log.debug("Volt mileage change: Volt_Mileage_Total=[{}]".format(total_mileage))

    energy_start = PersistenceExtensions.historicState(ir.getItem("Outlet9_Energy"), date_of_last_full_charge).state.floatValue()
    volt_mileage.log.debug("Volt mileage change: energy_start=[{}]".format(energy_start))
    energy_now = items["Outlet9_Energy"].floatValue()
    volt_mileage.log.debug("Volt mileage change: energy_now=[{}]".format(energy_now))
    energy_drive = energy_now - energy_start
    volt_mileage.log.debug("Volt mileage change: energy_drive=[{}]".format(energy_drive))
    events.sendCommand("Volt_Energy", str(energy_drive))
    events.sendCommand("Volt_Energy_Total", str(items["Volt_Energy_Total"].floatValue() + energy_drive))

    global ENERGY_COST_PER_KWH
    energy_drive_cost = ENERGY_COST_PER_KWH * energy_drive
    volt_mileage.log.debug("Volt mileage change: energy_drive_cost=[{:.3f}]".format(energy_drive_cost))
    events.sendCommand("Volt_Cost", str(energy_drive_cost))
    events.sendCommand("Volt_Cost_Total", str(items["Volt_Energy_Total"].floatValue() + energy_drive_cost))

    cost_per_mile = energy_drive_cost / previous_mileage
    volt_mileage.log.debug("Volt mileage change: cost_per_mile=[{:.3f}]".format(cost_per_mile))
    events.sendCommand("Volt_Cost_PerMile", str(cost_per_mile))

    cost_per_mile_ave = (items["Volt_Cost_PerMile_Average"].floatValue() + cost_per_mile) / 2
    volt_mileage.log.debug("Volt mileage change: cost_per_mile_ave=[{:.1f}]".format(cost_per_mile_ave))
    events.sendCommand("Volt_Cost_PerMile_Average", str(cost_per_mile_ave))

    efficiency = 1000 * energy_drive / previous_mileage
    volt_mileage.log.debug("Volt mileage change: efficiency=[{:.0f}]".format(efficiency))
    events.sendCommand("Volt_Efficiency", str(efficiency))

    efficiency_ave = (items["Volt_Efficiency_Average"].floatValue() + efficiency) / 2
    volt_mileage.log.debug("Volt mileage change: efficiency_ave=[{:.0f}]".format(efficiency_ave))
    events.sendCommand("Volt_Efficiency_Average", str(efficiency_ave))

    message = "Previous Volt charge report: {:.1f} cents per mile, {:.0f} Watt hours per mile".format(100 * cost_per_mile, efficiency)
    volt_mileage.log.debug("Volt mileage change: efficiency_ave=[{}]".format(efficiency_ave))
    notification("Volt mileage change", message)
    events.sendCommand("Outlet9", "ON")

Yes. This is in the docs.
I understood you wanted to capture the kWh reading at the start of a charge cycle, and then capture again at the completion.
If that is what you intend, choose triggers with care.

Thanks for all the input. Impressive - but too complex for my current intention.

I now successfully finished the rules required - and they seem to work when in dryrun mode.

// ***************************************************************************************************
rule "ChargeStart Detection"
when
    Item EVchargePower received update
then
    if ((EVchargePower.state > 1800) && Car_Charging.state == OFF)
           { Car_Charging.sendCommand(ON)
             EVchargeStartMeter.postUpdate(EVchargeWhImp.state)
           }
end

// ***************************************************************************************************
rule "ChargeStop Detection"
when
    Item EVchargePower received update
then
    if (Car_Charging.state == ON) { EVsessionMeter.postUpdate((EVchargeWhImp.state as DecimalType) - (EVchargeStartMeter.state as DecimalType)) }

    if ((EVchargePower.state < 10) && Car_Charging.state == ON)
           { Car_Charging.sendCommand(OFF)
             sendPushoverMessage(pushoverBuilder("Charging finished | " + EVsessionMeter.state + "kWh").withTitle("IONIQ"))
           }
end

// ***************************************************************************************************
1 Like

I’ll note that both rules have the same trigger.
That’s fine, but also a clue that you could combine into one rule.
Both rules revolve around if() - for combining, you might find an if-else structure useful

if (xx) {
   // do something
} else if (yy) {
   // do something different
}

I’ll note further, the trigger is for received update. As you noted earlier, updates to the same value are perfectly possible and sensible. As such, your rules could run frequently without anything changing (and so no useful work to be done).
If your rule only needs to pay attention if something actually changed, there is an alternative rule trigger available for changed

One last note; your EVchargeStartMeter Item will get updated with every pass through the rule. Did you intend to only capture that value at the start of the charge cycle? You could test to see if the “charging” Item was already ON - if it is, you’ve already done the start capture.

Thanks!

You’re right - will combine in a single rule instead of two.

The EVchargeStartMeter will only be updated once i hope. I guess you mean the EVsessionMeter item?
That is intended to see the currently charged amount per session always up to date…so how much the car got already into the battery. I hope i understand it correctly. Thanks!!!

btw, is there a way to have a switch item also visible in my sitemap but the user is not allowed to change it by themselves? This would apply for my Car_Charging switch item.

Yes do this:

Text item=mySwitchItem

are you sure, i tried in the sitemap to change switch item = …to… Text item= and in the GUI you now see this paperscript symbol plus nothing at all on the right side. Did i do something wrong?

Yes I am sure
Please post the item definition
Thanks

hm…
this is the item definition:

Switch Car_Charging "CarCharging"

Here we have the sitemap entry:

Text item=Car_Charging

Hope i did not do something too stupid…

You get the little paper icon by default, because you haven’t specified any icon. Small problem.

Bigger problem, you haven’t asked for any display of the Item state, so you don’t get one. Try

Switch Car_Charging "CarCharging [%s]"
1 Like

Thanks, it works that way - but now there is no switch anymore but a simple text field. What i more was looking for is a way to still see the switch but being not able to change it manually.

Do you know if this is somehow possible?

Not possible in the sitemap based UIs. There is a fairly comprehensive set of widgets, but no means to make custom widgets.

The HABpanel UI allows extensive custom widgets.

In the sitemap usage, you can of course use icons to reflect the state (but they appear on the left). You can colour the text, label and/or state areas. You can make the line disappear when it is in one state and reappear in another. You can use a MAP transform on the Item state to substitute fancy characters into the right hand area (and also colour them).
But you can’t put an arbitrary image on the right hand side, unless you can encode it as text.

Thanks!!

one last question, as e.g. after changing config files the item will go to “NULL” m rules run (i guess) into a problem. What is the smartest way to overcome this…i already did a rule on systemstart…but this seems to not trigger when e.g. only config files change. would it make sense to change it that way…so if it is NULL when charging starts…it would immediately go to ON and from there its no problem again ???

// ***************************************************************************************************
    rule "ChargeStart Detection"
    when
        Item EVchargePower received update
    then
        if ((EVchargePower.state > 1000) && (Car_Charging.state == OFF) || (Car_Charging.state == NULL)
               { Car_Charging.sendCommand(ON)
                 EVchargeStartMeter.postUpdate(EVchargeWhImp.state)
               }
    end

Yes. They’ll do the same at system boot time, unless you persist and restore them, or unless the binding is able to populate them before your rules run.

It’s not demanding to structure your rules not to use invalid data, and will likely pay dividends in situations you haven’t thought of yet.

if (someItem.state != NULL && otherItem.state != NULL) {
   // woohoo, both states I want to process are valid
} else {
   logInfo("test", "One Item at least is not ready, skipping processing")
}

Note that some bindings can also assign state UNDEF in the event of problems, like a communication failure.

Sometimes just use of != not-equals is powerful

if (someItem.state == OFF) {
   // this won't work for NULL or UNDEF
}

if (someItem.state != ON) {
   // this will work for OFF, NULL or UNDEF i.e. not-ON
}
1 Like