/* * SchedulerController.java - Copyright(c) 2013 Joe Pasqua * Provided under the MIT License. See the LICENSE file for details. * Created: Sep 7, 2013 */ package org.noroomattheinn.visibletesla; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.logging.Level; import java.util.prefs.Preferences; import javafx.collections.ObservableList; import javafx.collections.ObservableMap; import javafx.fxml.FXML; import javafx.scene.Node; import javafx.scene.control.TextArea; import javafx.scene.layout.GridPane; import org.noroomattheinn.tesla.ChargeState; import org.noroomattheinn.tesla.Result; import org.noroomattheinn.tesla.Vehicle; import org.noroomattheinn.utils.MailGun; import org.noroomattheinn.utils.ThreadManager; import org.noroomattheinn.utils.ThreadManager.Stoppable; import org.noroomattheinn.visibletesla.ScheduleItem.Command; import static org.noroomattheinn.tesla.Tesla.logger; /** * FXML Controller class * * @author Joe Pasqua <joe at NoRoomAtTheInn dot org> */ public class SchedulerController extends BaseController implements ScheduleItem.ScheduleOwner, Stoppable { private static final int Safe_Threshold = 25; /*------------------------------------------------------------------------------ * * Internal State * *----------------------------------------------------------------------------*/ @FXML private GridPane gridPane; @FXML private TextArea activityLog; private Vehicle v; private final List<ScheduleItem> schedulers = new ArrayList<>(); /*------------------------------------------------------------------------------ * * Implementation of the Stoppable interface * *----------------------------------------------------------------------------*/ @Override public void stop() { for (ScheduleItem si : schedulers) { si.shutDown(); } ScheduleItem.stop(); } /*------------------------------------------------------------------------------ * * Implementation of the ScheduleOwner interface * *----------------------------------------------------------------------------*/ @Override public String getExternalKey() { return v.getVIN(); } @Override public Preferences getPreferences() { return prefs.storage(); } @Override public App app() { return app; } @Override public Prefs prefs() { return prefs; } @Override public boolean useDegreesF() { return vtVehicle.useDegreesF(); } @Override public void runCommand( ScheduleItem.Command command, double value, MessageTarget messageTarget) { if (command != ScheduleItem.Command.SLEEP) { if (!vtVehicle.forceWakeup()) { logActivity("Can't wake vehicle - aborting", true); return; } app.api.setActive(); } if (!safeToRun(command)) return; if (!tryCommand(command, value, messageTarget)) { tryCommand(command, value, messageTarget); // Retry to avoid transient errors } } private boolean tryCommand( ScheduleItem.Command command, double value, MessageTarget messageTarget) { String name = ScheduleItem.commandToName(command); Result r = Result.Succeeded; boolean reportActvity = true; switch (command) { case CHARGE_SET: case CHARGE_ON: if (value > 0) { r = v.setChargePercent((int)value); if (!(r.success || r.explanation.equals("already_set"))) { logActivity("Unable to set charge target: " + r.explanation, true); } } if (command == Command.CHARGE_ON) r = v.startCharging(); break; case CHARGE_OFF: r = v.stopCharging(); break; case HVAC_ON: if (value > 0) { // Set the target temp first if (vtVehicle.useDegreesF()) r = v.setTempF(value, value); else r = v.setTempC(value, value); if (!r.success) break; } r = v.startAC(); break; case HVAC_OFF: r = v.stopAC();break; case AWAKE: app.api.stayAwake(); break; case SLEEP: app.api.allowSleeping(); break; case UNPLUGGED: r = unpluggedTrigger(); reportActvity = false; break; case MESSAGE: r = sendMessage(messageTarget); reportActvity = false; break; } if (value > 0) name = String.format("%s (%3.1f)", name, value); String entry = String.format("%s: %s", name, r.success ? "succeeded" : "failed"); if (!r.success) entry = entry + ", " + r.explanation; logActivity(entry, reportActvity); return r.success; } private Result sendMessage(MessageTarget messageTarget) { if (messageTarget == null) { MailGun.get().send( prefs.notificationAddress.get(), "No subject was specified", "No body was specified"); return Result.Succeeded; } MessageTemplate body = new MessageTemplate(messageTarget.getActiveMsg()); MessageTemplate subj = new MessageTemplate(messageTarget.getActiveSubj()); boolean sent = MailGun.get().send( messageTarget.getActiveEmail(), // To subj.getMessage(app.api, vtVehicle, null), // Subject body.getMessage(app.api, vtVehicle, null)); // Body return sent ? Result.Succeeded : Result.Failed; } private boolean requiresSafeMode(ScheduleItem.Command command) { return (command == ScheduleItem.Command.HVAC_ON); } private boolean safeToRun(ScheduleItem.Command command) { if (!requiresSafeMode(command)) return true; String name = ScheduleItem.commandToName(command); if (prefs.safeIncludesMinCharge.get()) { if (vtVehicle.chargeState.get().batteryPercent < Safe_Threshold) { String entry = String.format( "%s: Insufficient charge - aborted", name); logActivity(entry, true); return false; } } if (prefs.safeIncludesPluggedIn.get()) { String msg; switch (vtVehicle.chargeState.get().chargingState) { case Unknown: msg = String.format("%s: Can't tell if car is plugged in - aborted", name); logActivity(msg, true); return false; case Disconnected: msg = String.format("%s: Car is not plugged in - aborted", name); logActivity(msg, true); return false; default: return true; } } return true; } private synchronized Result unpluggedTrigger() { ChargeState charge = vtVehicle.chargeState.get(); ChargeState.Status status = charge.chargingState; if (status == ChargeState.Status.Disconnected) { MailGun.get().send( prefs.notificationAddress.get(), "Your car is not plugged in. Range = " + (int)charge.range); return new Result(true, "Vehicle is unplugged. Notification sent"); } else if (status == ChargeState.Status.Unknown) { MailGun.get().send( prefs.notificationAddress.get(), "Can't determine if your car is plugged in. Please check"); return new Result(true, "Can't tell if car is plugged in. Warning sent"); } return new Result(true, "Vehicle is plugged-in. No notification sent"); } /*------------------------------------------------------------------------------ * * PRIVATE - Loading the UI Elements * *----------------------------------------------------------------------------*/ private void prepareSchedulerUI(GridPane gridPane) { Map<Integer,Map<Integer,Node>> rows = loadFromGrid(gridPane); for (Map.Entry<Integer, Map<Integer, Node>> rowEntry : rows.entrySet()) { int rowNum = rowEntry.getKey().intValue(); Map<Integer, Node> row = rowEntry.getValue(); schedulers.add(new ScheduleItem(rowNum, row, this)); } } private Map<Integer,Map<Integer,Node>> loadFromGrid(GridPane gp) { Map<Integer,Map<Integer,Node>> rowMap = new HashMap<>(); ObservableList<Node> kids = gp.getChildren(); for (Node kid : kids) { ObservableMap<Object,Object> props = kid.getProperties(); int columnNumber = getRowOrColumn(kid, false); int rowNumber = getRowOrColumn(kid, true); if (rowNumber < 0) continue; // -1 isn't in the grid rowNumber--; Map<Integer,Node> thisRow = rowMap.get(rowNumber); if (thisRow == null) { thisRow = new HashMap<>(); rowMap.put(rowNumber, thisRow); } thisRow.put(columnNumber, kid); } return rowMap; } private int getRowOrColumn(Node node, boolean getRow) { ObservableMap<Object,Object> props = node.getProperties(); String propName = getRow ? "gridpane-row" : "gridpane-column"; Number prop = ((Number)props.get(propName)); return (prop == null) ? -1 : prop.intValue(); } /*------------------------------------------------------------------------------ * * Methods overridden from BaseController * *----------------------------------------------------------------------------*/ @Override protected void fxInitialize() { // Deep-Six the refresh button and progress indicator refreshButton.setDisable(true); refreshButton.setVisible(false); progressIndicator.setVisible(false); prepareSchedulerUI(gridPane); } @Override protected void initializeState() { v = vtVehicle.getVehicle(); ThreadManager.get().addStoppable(this); } @Override protected void activateTab() { for (ScheduleItem item : schedulers) { item.loadExistingSchedule(); } } @Override protected void refresh() { } /*------------------------------------------------------------------------------ * * PRIVATE - Utility Methods * *----------------------------------------------------------------------------*/ private void logActivity(String entry, boolean report) { Date now = new Date(); String previousEntries = activityLog.getText(); String datedEntry = String.format( "[%1$tm/%1$td/%1$ty %1$tH:%1$tM] %2$s\n%3$s", now, entry, previousEntries); activityLog.setText(datedEntry); logger.log(Level.FINE, entry); if (report) { app.schedulerActivity.set(entry); } } }