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
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")