com.imaginary.home.controller.HomeController.java Source code

Java tutorial

Introduction

Here is the source code for com.imaginary.home.controller.HomeController.java

Source

/**
 * Copyright (C) 2013 George Reese
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
    
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.imaginary.home.controller;

import com.imaginary.home.lighting.Light;
import com.imaginary.home.lighting.LightingService;
import org.dasein.util.CalendarWrapper;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import javax.annotation.Nonnegative;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.TimeZone;
import java.util.TreeSet;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

public class HomeController {
    static public final ExecutorService executorService = Executors.newCachedThreadPool();

    static public final String COMMAND_FILE;
    static public final String CONFIG_FILE;
    static public final String SCHEDULER_FILE;

    static {
        String root = System.getProperty("ihaCfgRoot", "/etc/imaginary");

        COMMAND_FILE = root + "/iha/command.cfg";
        CONFIG_FILE = root + "/iha/iha.cfg";
        SCHEDULER_FILE = root + "/iha/schedule.cfg";
    }

    static private HomeController homeController;

    static public @Nonnull String formatDate(long timestamp) {
        return formatDate(new Date(timestamp));
    }

    static public @Nonnull String formatDate(@Nonnull Date when) {
        SimpleDateFormat fmt = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
        Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("UTC"));

        cal.setTime(when);
        return fmt.format(cal.getTime());
    }

    static public @Nonnull HomeController getInstance() throws ControllerException {
        if (homeController == null) {
            try {
                homeController = new HomeController();
            } catch (Exception e) {
                e.printStackTrace();
                throw new ControllerException("Unable to load system: " + e.getMessage());
            }
        }
        return homeController;
    }

    static public long parseDate(@Nonnull String timestring) throws ParseException {
        SimpleDateFormat fmt = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");

        return (fmt.parse(timestring)).getTime();
    }

    private Map<String, HomeAutomationSystem> automationSystems;
    private List<CloudService> cloudServices;
    private final LinkedList<CommandList> commandQueue = new LinkedList<CommandList>();
    private long lastLoad = 0L;
    private String name;
    private boolean running = false;
    private TreeSet<ScheduledCommandList> scheduler;

    private HomeController() throws JSONException, ClassNotFoundException, IllegalAccessException,
            InstantiationException, IOException {
        automationSystems = new HashMap<String, HomeAutomationSystem>();
        scheduler = new TreeSet<ScheduledCommandList>();
        cloudServices = new ArrayList<CloudService>();
        loadConfiguration();
        loadCommands();
        loadSchedule();
        executorService.submit(new Callable<Boolean>() {
            @Override
            public Boolean call() throws Exception {
                processCommands();
                return true;
            }
        });
        executorService.submit(new Callable<Boolean>() {
            @Override
            public Boolean call() throws Exception {
                checkScheduler();
                return true;
            }
        });
        executorService.submit(new Callable<Boolean>() {
            @Override
            public Boolean call() throws Exception {
                while (true) {
                    synchronized (commandQueue) {
                        if (!running) {
                            return true;
                        }
                        try {
                            commandQueue.wait(CalendarWrapper.MINUTE);
                        } catch (InterruptedException ignore) {
                        }
                        File f = new File(CONFIG_FILE);

                        if (f.lastModified() > lastLoad) {
                            try {
                                loadConfiguration();
                            } catch (Throwable t) {
                                t.printStackTrace();
                            }
                        }
                    }
                }
            }
        });
        for (final CloudService service : cloudServices) {
            executorService.submit(new Callable<Boolean>() {
                @Override
                public Boolean call() throws Exception {
                    poll(service);
                    return true;
                }
            });
        }
    }

    public void cancelScheduledCommands(@Nonnull String... havingCommandIds) throws ControllerException {
        synchronized (commandQueue) {
            if (!running) {
                return;
            }
            for (ScheduledCommandList list : scheduler) {
                boolean matches = false;

                for (JSONObject cmd : list) {
                    for (String id : havingCommandIds) {
                        try {
                            if (cmd.getString("id").equals(id)) {
                                matches = true;
                                break;
                            }
                        } catch (JSONException e) {
                            throw new ControllerException(e);
                        }
                    }
                    if (matches) {
                        break;
                    }
                }
                if (matches) {
                    scheduler.remove(list);
                    saveSchedule();
                    return;
                }
            }
        }
    }

    private void checkScheduler() {
        while (true) {
            synchronized (commandQueue) {
                if (!running) {
                    return;
                }
            }
            ScheduledCommandList cmdList;

            do {
                synchronized (commandQueue) {
                    if (!scheduler.isEmpty()) {
                        cmdList = scheduler.iterator().next();
                        scheduler.remove(cmdList);
                        try {
                            saveSchedule();
                        } catch (Throwable t) {
                            t.printStackTrace();
                        }
                    } else {
                        cmdList = null;
                    }
                }
                if (cmdList != null && cmdList.getExecuteAfter() <= System.currentTimeMillis()) {
                    ArrayList<Future<Boolean>> results = new ArrayList<Future<Boolean>>();
                    boolean[] completions = new boolean[results.size()];
                    CloudService service = getService(cmdList.getServiceId());

                    if (service == null) {
                        continue;
                    }
                    for (JSONObject cmd : cmdList) {
                        try {
                            Command c = new Command(cmd);

                            results.add(c.start());
                        } catch (JSONException e) {
                            results.add(null);
                        }
                    }
                    boolean done;

                    do {
                        done = true;
                        for (int i = 0; i < completions.length; i++) {
                            if (!completions[i]) {
                                Future<Boolean> f = results.get(i);
                                JSONObject cmd = cmdList.get(i);

                                if (f == null) {
                                    try {
                                        service.postResult(cmd.getString("id"), false,
                                                new JSONException("Invalid JSON in command"));
                                    } catch (Throwable t) {
                                        t.printStackTrace();
                                    } finally {
                                        completions[i] = true;
                                    }
                                }
                                if (f.isDone()) {
                                    Throwable failure = null;
                                    boolean result = false;

                                    try {
                                        result = f.get();
                                    } catch (Throwable t) {
                                        failure = t;
                                    }
                                    try {
                                        service.postResult(cmd.getString("id"), result, failure);
                                    } catch (Throwable t) {
                                        t.printStackTrace();
                                    } finally {
                                        completions[i] = true;
                                    }
                                } else {
                                    done = false;
                                }
                            }
                        }
                    } while (!done);
                }
            } while (cmdList != null);
            synchronized (commandQueue) {
                try {
                    commandQueue.wait(60000L);
                } catch (InterruptedException ignore) {
                }
            }
        }
    }

    public @Nonnull String getName() {
        return name;
    }

    public @Nullable CloudService getService(@Nonnull String id) {
        for (CloudService svc : cloudServices) {
            if (svc.getServiceId().equals(id)) {
                return svc;
            }
        }
        return null;
    }

    public @Nullable HomeAutomationSystem getSystem(@Nonnull String id) {
        return automationSystems.get(id);
    }

    private void loadCommands() throws IOException, JSONException {
        synchronized (commandQueue) {
            BufferedReader reader;

            try {
                reader = new BufferedReader(new InputStreamReader(new FileInputStream(COMMAND_FILE)));
            } catch (IOException e) {
                return;
            }
            StringBuilder json = new StringBuilder();
            String line;

            while ((line = reader.readLine()) != null) {
                json.append(line);
                json.append(" ");
            }
            JSONObject cfg = new JSONObject(json.toString());
            if (cfg.has("commands")) {
                JSONArray list = cfg.getJSONArray("commands");

                for (int i = 0; i < list.length(); i++) {
                    JSONObject cmd = list.getJSONObject(i);
                    JSONObject[] commands;
                    String serviceId;

                    if (cmd.has("serviceId")) {
                        serviceId = cmd.getString("serviceId");
                    } else {
                        continue;
                    }
                    if (cmd.has("commands")) {
                        JSONArray cmds = cmd.getJSONArray("commands");

                        commands = new JSONObject[cmds.length()];
                        for (int j = 0; j < cmds.length(); j++) {
                            commands[j] = cmds.getJSONObject(j);
                        }
                    } else {
                        continue;
                    }
                    commandQueue.push(new CommandList(serviceId, commands));
                }
            }
            try {
                saveCommands(new ArrayList<CommandList>());
            } catch (Throwable t) {
                if (commandQueue.isEmpty()) {
                    return;
                }
                throw new IOException("Failed to save empty commands: " + t.getMessage());
            }
        }
    }

    private void loadConfiguration() throws JSONException, ClassNotFoundException, IllegalAccessException,
            InstantiationException, IOException {
        synchronized (commandQueue) {
            BufferedReader reader;

            try {
                reader = new BufferedReader(new InputStreamReader(new FileInputStream(CONFIG_FILE)));
            } catch (IOException e) {
                e.printStackTrace();
                return;
            }
            HashMap<String, HomeAutomationSystem> automationSystems = new HashMap<String, HomeAutomationSystem>();
            ArrayList<CloudService> services = new ArrayList<CloudService>();
            StringBuilder json = new StringBuilder();
            String line;

            while ((line = reader.readLine()) != null) {
                json.append(line);
                json.append(" ");
            }
            JSONObject cfg = new JSONObject(json.toString());

            if (cfg.has("name") && !cfg.isNull("name")) {
                name = cfg.getString("name");
            } else {
                name = "Imaginary Home Controller Relay";
            }
            if (cfg.has("systems")) {
                JSONArray list = cfg.getJSONArray("systems");

                for (int i = 0; i < list.length(); i++) {
                    JSONObject sys = list.getJSONObject(i);

                    String cname = sys.getString("cname");
                    String id = sys.getString("id");

                    HomeAutomationSystem system = (HomeAutomationSystem) Class.forName(cname).newInstance();
                    Properties auth = new Properties();
                    Properties custom = new Properties();

                    JSONObject p;

                    if (sys.has("authenticationProperties") && !sys.isNull("authenticationProperties")) {
                        p = sys.getJSONObject("authenticationProperties");
                        String[] names = JSONObject.getNames(p);

                        if (names != null) {
                            for (String key : names) {
                                auth.put(key, p.getString(key));
                            }
                        }
                    }
                    if (sys.has("customProperties") && !sys.isNull("customProperties")) {
                        p = sys.getJSONObject("customProperties");
                        String[] names = JSONObject.getNames(p);

                        if (names != null) {
                            for (String key : names) {
                                auth.put(key, p.getString(key));
                            }
                        }
                    }
                    system.init(id, auth, custom);
                    automationSystems.put(id, system);
                }
            }
            if (cfg.has("services")) {
                JSONArray list = cfg.getJSONArray("services");

                for (int i = 0; i < list.length(); i++) {
                    JSONObject svc = list.getJSONObject(i);
                    String name, endpoint, id, secret, proxyHost = null;
                    int proxyPort = 0;

                    if (svc.has("endpoint")) {
                        endpoint = svc.getString("endpoint");
                    } else {
                        continue;
                    }
                    if (svc.has("id")) {
                        id = svc.getString("id");
                    } else {
                        continue;
                    }
                    if (svc.has("apiKeySecret")) {
                        secret = svc.getString("apiKeySecret");
                    } else {
                        continue;
                    }
                    if (svc.has("name")) {
                        name = svc.getString("name");
                    } else {
                        name = endpoint;
                    }
                    if (svc.has("proxyHost") && !svc.isNull("proxyHost")) {
                        proxyHost = svc.getString("proxyHost");
                    }
                    if (svc.has("proxyPort") && !svc.isNull("proxyPort")) {
                        proxyPort = svc.getInt("proxyPort");
                    }
                    services.add(new CloudService(id, secret, name, endpoint, proxyHost, proxyPort));
                }
            }
            this.cloudServices = services;
            this.automationSystems = automationSystems;
            lastLoad = System.currentTimeMillis();
        }
    }

    private void loadSchedule() throws IOException, JSONException {
        synchronized (commandQueue) {
            BufferedReader reader;

            try {
                reader = new BufferedReader(new InputStreamReader(new FileInputStream(SCHEDULER_FILE)));
            } catch (IOException e) {
                // probably does not exist, which is OK
                return;
            }
            StringBuilder json = new StringBuilder();
            String line;

            while ((line = reader.readLine()) != null) {
                json.append(line);
                json.append(" ");
            }
            JSONObject cfg = new JSONObject(json.toString());
            if (cfg.has("schedule")) {
                JSONArray list = cfg.getJSONArray("schedule");

                for (int i = 0; i < list.length(); i++) {
                    JSONObject scheduledList = list.getJSONObject(i);
                    JSONObject[] commands = null;
                    long executeAfter = 0L;
                    String serviceId;
                    String scheduleId;

                    if (scheduledList.has("serviceId")) {
                        serviceId = scheduledList.getString("serviceId");
                    } else {
                        continue;
                    }
                    if (scheduledList.has("scheduleId")) {
                        scheduleId = scheduledList.getString("scheduleId");
                    } else {
                        continue;
                    }
                    if (scheduledList.has("executeAfter")) {
                        try {
                            executeAfter = parseDate(scheduledList.getString("executeAfter"));
                        } catch (ParseException e) {
                            // this should not happen
                            e.printStackTrace();
                            continue;
                        }
                    }
                    if (scheduledList.has("commands")) {
                        JSONArray cmds = scheduledList.getJSONArray("commands");
                        if (cmds.length() < 1) {
                            continue;
                        }
                        commands = new JSONObject[cmds.length()];

                        for (int j = 0; j < cmds.length(); j++) {
                            commands[j] = cmds.getJSONObject(j);
                        }
                    }
                    if (commands == null || executeAfter < System.currentTimeMillis()) {
                        continue;
                    }
                    scheduler.add(new ScheduledCommandList(serviceId, scheduleId, executeAfter, commands));
                }
            }
        }
    }

    public @Nonnull Iterable<ManagedResource> listResources() throws CommunicationException {
        ArrayList<ManagedResource> resources = new ArrayList<ManagedResource>();

        for (HomeAutomationSystem system : listSystems()) {
            if (system instanceof LightingService) {
                for (Light light : ((LightingService) system).listLights()) {
                    resources.add(light);
                }
            }
        }
        return resources;
    }

    public @Nonnull Collection<HomeAutomationSystem> listSystems() {
        return automationSystems.values();
    }

    public @Nonnull String pairService(@Nonnull String name, @Nonnull String endpoint, @Nullable String proxyHost,
            int proxyPort, @Nonnull String pairingToken) throws ControllerException, CommunicationException {
        CloudService service = CloudService.pair(name, endpoint, proxyHost, proxyPort, pairingToken);

        synchronized (commandQueue) {
            try {
                loadConfiguration();
            } catch (Exception e) {
                throw new ControllerException("Failed to load current system state: " + e.getMessage());
            }
            cloudServices.add(service);
            saveConfiguration();
        }
        return service.getServiceId();
    }

    public @Nonnull String pairSystem(@Nonnull String cname, @Nonnull Properties authProperties,
            @Nonnull Properties customProperties) throws ControllerException, CommunicationException {
        try {
            HomeAutomationSystem system = (HomeAutomationSystem) Class.forName(cname).newInstance();
            String id = UUID.randomUUID().toString();

            system.init(id, authProperties, customProperties);
            system.pair("IHA");
            synchronized (commandQueue) {
                try {
                    loadConfiguration();
                } catch (Exception e) {
                    throw new ControllerException("Failed to load current system state: " + e.getMessage());
                }
                automationSystems.put(system.getId(), system);
                saveConfiguration();
            }
            return id;
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
            throw new CommunicationException("No such automation system: " + cname);
        } catch (InstantiationException e) {
            e.printStackTrace();
            throw new CommunicationException("Invalid automation system: " + cname);
        } catch (IllegalAccessException e) {
            e.printStackTrace();
            throw new CommunicationException("Invalid automation system: " + cname);
        }
    }

    private void poll(@Nonnull CloudService service) {
        long pollWait = CalendarWrapper.MINUTE;
        long nextState = 0L;

        while (true) {
            synchronized (commandQueue) {
                try {
                    commandQueue.wait(pollWait);
                } catch (InterruptedException ignore) {
                }
                try {
                    if (!running) {
                        return;
                    }
                    if (System.currentTimeMillis() > nextState) {
                        boolean hasCommands = service.postState();

                        nextState = System.currentTimeMillis() + CalendarWrapper.MINUTE;
                        if (hasCommands) {
                            service.fetchCommands();
                            pollWait = (10L * CalendarWrapper.SECOND);
                        } else {
                            pollWait = CalendarWrapper.MINUTE;
                        }
                    } else {
                        if (service.hasCommands()) {
                            service.fetchCommands();
                            pollWait = (10L * CalendarWrapper.SECOND);
                        } else {
                            pollWait = pollWait * 2;
                            if (pollWait > CalendarWrapper.MINUTE) {
                                pollWait = CalendarWrapper.MINUTE;
                            }
                        }
                    }
                } catch (Throwable t) {
                    t.printStackTrace();
                }
            }
        }
    }

    private void processCommands() {
        synchronized (commandQueue) {
            running = true;
        }
        while (true) {
            synchronized (commandQueue) {
                if (!running) {
                    return;
                }
            }
            CommandList cmdList;

            do {
                synchronized (commandQueue) {
                    if (!commandQueue.isEmpty()) {
                        cmdList = commandQueue.poll();
                    } else {
                        cmdList = null;
                    }
                }
                if (cmdList != null) {
                    CloudService service = getService(cmdList.getServiceId());

                    if (service == null) {
                        continue;
                    }
                    ArrayList<Future<Boolean>> results = new ArrayList<Future<Boolean>>();
                    boolean[] completions = new boolean[results.size()];

                    for (JSONObject cmd : cmdList) {
                        try {
                            Command c = new Command(cmd);

                            results.add(c.start());
                        } catch (JSONException e) {
                            results.add(null);
                        }
                    }
                    boolean done;

                    do {
                        done = true;
                        for (int i = 0; i < completions.length; i++) {
                            if (!completions[i]) {
                                Future<Boolean> f = results.get(i);
                                JSONObject cmd = cmdList.get(i);

                                if (f == null) {
                                    try {
                                        service.postResult(cmd.getString("id"), false,
                                                new JSONException("Invalid JSON in command"));
                                    } catch (Throwable t) {
                                        t.printStackTrace();
                                    } finally {
                                        completions[i] = true;
                                    }
                                } else if (f.isDone()) {
                                    Throwable failure = null;
                                    boolean result = false;

                                    try {
                                        result = f.get();
                                    } catch (Throwable t) {
                                        failure = t;
                                    }
                                    try {
                                        service.postResult(cmd.getString("id"), result, failure);
                                    } catch (Throwable t) {
                                        t.printStackTrace();
                                    } finally {
                                        completions[i] = true;
                                    }
                                } else {
                                    done = false;
                                }
                            }
                        }
                    } while (!done);
                }
            } while (cmdList != null);
            synchronized (commandQueue) {
                try {
                    commandQueue.wait(30000L);
                } catch (InterruptedException ignore) {
                }
            }
        }
    }

    public void queueCommands(@Nonnull CloudService service, @Nonnull JSONObject... commands)
            throws ControllerException {
        synchronized (commandQueue) {
            if (!running) {
                throw new ControllerException("Not currently accepting new commands");
            }
            commandQueue.push(new CommandList(service.getServiceId(), commands));
            commandQueue.notifyAll();
        }
    }

    private void saveCommands(List<CommandList> toSave) throws ControllerException {
        synchronized (commandQueue) {
            ArrayList<Map<String, Object>> all = new ArrayList<Map<String, Object>>();
            HashMap<String, Object> cfg = new HashMap<String, Object>();

            for (CommandList cmdList : toSave) {
                ArrayList<Map<String, Object>> commands = new ArrayList<Map<String, Object>>();
                HashMap<String, Object> map = new HashMap<String, Object>();

                for (JSONObject cmd : cmdList) {
                    try {
                        commands.add(toMap(cmd));
                    } catch (JSONException e) {
                        throw new ControllerException(e);
                    }
                }
                map.put("commands", commands);
                map.put("serviceId", cmdList.getServiceId());
                all.add(map);
            }
            cfg.put("commands", all);
            try {
                File f = new File(COMMAND_FILE);
                File backup = null;

                if (f.exists()) {
                    backup = new File(COMMAND_FILE + "." + System.currentTimeMillis());
                    if (!f.renameTo(backup)) {
                        throw new ControllerException("Unable to make backup of configuration file");
                    }
                    f = new File(COMMAND_FILE);
                }
                boolean success = false;
                try {
                    BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(f)));

                    writer.write((new JSONObject(cfg)).toString());
                    writer.newLine();
                    writer.flush();
                    writer.close();
                    success = true;
                } finally {
                    if (!success && backup != null) {
                        //noinspection ResultOfMethodCallIgnored
                        backup.renameTo(f);
                    }
                }
            } catch (IOException e) {
                throw new ControllerException("Unable to save command file: " + e.getMessage());
            }
        }
    }

    private void saveConfiguration() throws ControllerException {
        ArrayList<Map<String, Object>> all = new ArrayList<Map<String, Object>>();
        HashMap<String, Object> cfg = new HashMap<String, Object>();

        cfg.put("name", name);
        for (HomeAutomationSystem sys : listSystems()) {
            HashMap<String, Object> json = new HashMap<String, Object>();

            json.put("cname", sys.getClass().getName());
            json.put("id", sys.getId());
            json.put("authenticationProperties", sys.getAuthenticationProperties());
            json.put("customProperties", sys.getCustomProperties());
            all.add(json);
        }
        cfg.put("systems", all);
        all = new ArrayList<Map<String, Object>>();
        for (CloudService service : cloudServices) {
            HashMap<String, Object> json = new HashMap<String, Object>();

            json.put("id", service.getServiceId());
            json.put("name", service.getName());
            json.put("endpoint", service.getEndpoint());
            json.put("apiKeySecret", service.getApiKeySecret());
            if (service.getProxyHost() != null) {
                json.put("proxyHost", service.getProxyHost());
                json.put("proxyPort", service.getProxyPort());
            }
            all.add(json);
        }
        cfg.put("services", all);
        try {
            File f = new File(CONFIG_FILE);
            File backup = null;

            if (f.exists()) {
                backup = new File(CONFIG_FILE + "." + System.currentTimeMillis());
                if (!f.renameTo(backup)) {
                    throw new ControllerException("Unable to make backup of configuration file");
                }
                f = new File(CONFIG_FILE);
            }
            boolean success = false;
            try {
                BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(f)));

                writer.write((new JSONObject(cfg)).toString());
                writer.newLine();
                writer.flush();
                writer.close();
                success = true;
            } finally {
                if (!success && backup != null) {
                    //noinspection ResultOfMethodCallIgnored
                    backup.renameTo(f);
                }
            }
        } catch (IOException e) {
            throw new ControllerException("Unable to save configuration: " + e.getMessage());
        }
    }

    private void saveSchedule() throws ControllerException {
        synchronized (commandQueue) {
            ArrayList<Map<String, Object>> all = new ArrayList<Map<String, Object>>();
            HashMap<String, Object> cfg = new HashMap<String, Object>();

            for (ScheduledCommandList sList : scheduler) {
                ArrayList<Map<String, Object>> commands = new ArrayList<Map<String, Object>>();
                HashMap<String, Object> schedule = new HashMap<String, Object>();

                for (JSONObject cmd : sList) {
                    try {
                        commands.add(toMap(cmd));
                    } catch (JSONException e) {
                        throw new ControllerException(e);
                    }
                }
                schedule.put("commands", commands);
                schedule.put("executeAfter", sList.getExecuteAfter());
                schedule.put("scheduleId", sList.getScheduleId());
                schedule.put("serviceId", sList.getServiceId());
                all.add(schedule);
            }
            cfg.put("schedule", all);
            try {
                File f = new File(SCHEDULER_FILE);
                File backup = null;

                if (f.exists()) {
                    backup = new File(SCHEDULER_FILE + "." + System.currentTimeMillis());
                    if (!f.renameTo(backup)) {
                        throw new ControllerException("Unable to make backup of configuration file");
                    }
                    f = new File(SCHEDULER_FILE);
                }
                boolean success = false;
                try {
                    BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(f)));

                    writer.write((new JSONObject(cfg)).toString());
                    writer.newLine();
                    writer.flush();
                    writer.close();
                    success = true;
                } finally {
                    if (!success && backup != null) {
                        //noinspection ResultOfMethodCallIgnored
                        backup.renameTo(f);
                    }
                }
            } catch (IOException e) {
                throw new ControllerException("Unable to save command file: " + e.getMessage());
            }
        }
    }

    public void scheduleCommands(@Nonnull CloudService service, @Nonnull String scheduleId,
            @Nonnegative long executeAfter, @Nonnull JSONObject... commands) throws ControllerException {
        synchronized (commandQueue) {
            if (!running) {
                throw new ControllerException("Not currently accepting new commands");
            }
            if (executeAfter < System.currentTimeMillis()) {
                throw new ControllerException("Invalid execution time: " + formatDate(executeAfter));
            }
            scheduler.add(new ScheduledCommandList(service.getServiceId(), scheduleId, executeAfter, commands));
            saveSchedule();
            commandQueue.notifyAll();
        }
    }

    public void shutdown() {
        synchronized (commandQueue) {
            running = false;
            commandQueue.notifyAll();
            try {
                saveCommands(commandQueue);
            } catch (Throwable t) {
                t.printStackTrace();
            }
            executorService.shutdown();
            try {
                if (!executorService.awaitTermination(2, TimeUnit.MINUTES)) {
                    executorService.shutdownNow();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    private Map<String, Object> toMap(JSONObject j) throws JSONException {
        HashMap<String, Object> map = new HashMap<String, Object>();

        for (String key : JSONObject.getNames(j)) {
            map.put(key, toValue(j.get(key)));
        }
        return map;
    }

    private Object toValue(Object ob) throws JSONException {
        if (ob instanceof JSONObject) {
            return toMap((JSONObject) ob);
        } else if (ob instanceof JSONArray) {
            ArrayList<Object> converted = new ArrayList<Object>();
            JSONArray items = (JSONArray) ob;

            for (int i = 0; i < items.length(); i++) {
                converted.add(toValue(items.get(i)));
            }
            return converted;
        }
        return ob;
    }

    static public void main(String... args) throws Exception {
        if (args.length < 1) {
            System.err.println("No work");
        }
        String action = args[0];

        if (action.equalsIgnoreCase("pair")) {
            String name = args[1];
            String endpoint = args[2];
            String pairingToken = args[3];
            String proxyHost = null;
            int proxyPort = 0;

            if (args.length == 6) {
                proxyHost = args[4];
                proxyPort = Integer.parseInt(args[5]);
            }
            HomeController.getInstance().pairService(name, endpoint, proxyHost, proxyPort, pairingToken);
        } else if (action.equalsIgnoreCase("run")) {
            while (true) {
                try {
                    Thread.sleep(60000L);
                } catch (InterruptedException e) {
                }
            }
        } else {
            System.err.println("No such action: " + action);
        }
    }
}