src/main/java/lu/dainesch/huesense/hue/data/LightSensor.java
public class LightSensor extends Sensor<LightSensor.LightLevel> {

    private static final String INSERT_CUR_VAL = "Insert into LIGHT_DATA (SD_ID, LIGHT_LEVEL, IS_DARK, IS_DAYLIGHT) values (?, ?, ?, ?)";
    private static final String SELECT_RANGE = "Select l.SD_ID, l.LIGHT_LEVEL, l.IS_DARK, l.IS_DAYLIGHT, d.CREATED from LIGHT_DATA l "
            + "JOIN SENSOR_DATA d on d.SD_ID=l.SD_ID "
            + "where d.SENSOR_ID = ? and d.CREATED > ? and d.CREATED <= ? "
            + "order by d.created asc ";
    public static final String LUX_SUFFIX = " lux";
    private static final Logger LOG = LoggerFactory.getLogger(LightSensor.class);

    private final XYChart.Series<Date, Number> data;
    private final ObjectProperty<GraphInterval> graphInterval;

    public LightSensor(int id, String uniqueID, HueSenseConfig config, DBManager dbMan) {
        super(id, uniqueID, SensorType.LIGHT, config, dbMan);
        data = new XYChart.Series<>();
        data.setName("LightLevel");
        graphInterval = new SimpleObjectProperty<>(GraphInterval.INTERVALS.get(1));
    }

    @Override
    public void initSensor() {
        // load data
        setGraphInterval(GraphInterval.INTERVALS.get(1));
    }

    @Override
    public void updateSensor(JsonObject obj) throws UpdateException {
        try {
            setName(obj.getString("name"));

            JsonObject state = obj.getJsonObject("state");

            SimpleDateFormat sdf = new SimpleDateFormat(Constants.HUEDATEFORMAT);
            sdf.setTimeZone(TimeZone.getTimeZone("GMT"));
            Date time = sdf.parse(state.getString("lastupdated"));

            LightLevel level = new LightLevel(state.getInt("lightlevel"), state.getBoolean("dark"), state.getBoolean("daylight"));
            SensorValue<LightLevel> val = new SensorValue<>(time, level);
            updateValue(val);

            JsonObject conf = obj.getJsonObject("config");
            setOn(conf.getBoolean("on"));
            setBattery(conf.getInt("battery"));
            setReachable(conf.getBoolean("reachable"));

        } catch (ParseException | NullPointerException | ClassCastException ex) {
            throw new UpdateException("Error updating temp sensor", ex);
        }
    }

    @Override
    public void saveCurrentValueInDB(Long dataId) throws UpdateException {
        try (Connection conn = dbMan.getConnection()) {
            try (PreparedStatement stmt = conn.prepareStatement(INSERT_CUR_VAL)) {
                stmt.setLong(1, dataId);
                stmt.setInt(2, currentValue.getLevel());
                stmt.setBoolean(3, currentValue.isDark());
                stmt.setBoolean(4, currentValue.isDayLight());
                stmt.executeUpdate();
            }
        } catch (SQLException ex) {
            throw new UpdateException("Error updating presence data", ex);
        }
    }

    @Override
    public Set<SensorValue<LightLevel>> getValuesInRange(Date start, Date end) {
        NavigableSet<SensorValue<LightLevel>> ret = new TreeSet<>();

        try (Connection conn = dbMan.getConnection()) {
            try (PreparedStatement stmt = conn.prepareStatement(SELECT_RANGE)) {
                stmt.setLong(1, dbId);
                stmt.setTimestamp(2, new Timestamp(start.getTime()));
                long endTime = end == null ? System.currentTimeMillis() : end.getTime();
                stmt.setTimestamp(3, new Timestamp(endTime));
                ResultSet rs = stmt.executeQuery();
                while (rs.next()) {
                    LightLevel level = new LightLevel(rs.getInt("LIGHT_LEVEL"), rs.getBoolean("IS_DARK"), rs.getBoolean("IS_DAYLIGHT"));
                    SensorValue<LightLevel> val = new SensorValue<>(rs.getTimestamp("CREATED"), level);
                    ret.add(val);
                }
            }
        } catch (SQLException ex) {
            LOG.error("Error querying light values", ex);
        }
        return ret;
    }

    @Override
    public void updateView(SensorValue<LightLevel> val) {
        Duration maxAge = graphInterval.get().getDuration();
        while (!data.getData().isEmpty() && (System.currentTimeMillis() - data.getData().get(0).getXValue().getTime() > maxAge.toMillis())) {
            // remove old values
            data.getData().remove(0);
        }
        if (val == null || System.currentTimeMillis() - val.getTime().getTime() > maxAge.toMillis()) {
            // do not add old data
            return;
        }
        data.getData().add(new XYChart.Data<>(val.getTime(), toLux(val.getValue().getLevel())));
    }

    public XYChart.Series<Date, Number> getData() {
        return data;
    }

    @Override
    public String getValueAsString(LightLevel value) {
        return toLux(value.level) + LUX_SUFFIX;
    }

    public void setGraphInterval(GraphInterval val) {

        Date start = new Date(System.currentTimeMillis() - (long) val.getDuration().toMillis());
        data.getData().removeAll(data.getData());   // clear does not work
        graphInterval.set(val);
        getValuesInRange(start, null).forEach((s) -> {
            updateView(s);
        });

    }

    @Override
    public String[] getCSVHeader() {
        return new String[]{"Time", "Dark", "Daylight", "Raw Value", "Lux"};
    }

    @Override
    public String[] getCSVEntry(SensorValue<LightLevel> val, SimpleDateFormat sdf) {
        return new String[]{sdf.format(val.getTime()), String.valueOf(val.getValue().isDark()), String.valueOf(val.getValue().isDayLight()),
            String.valueOf(val.getValue().getLevel()), String.valueOf(toLux(val.getValue().getLevel()))};
    }

    private static BigDecimal toLux(int input) {
        // linear interpolation from table in api docs
        double val = (double) input;
        double ret = 0;
        if (val > 1 && val <= 3000) {
            ret = val / 2999d + 2998d / 2999d;
        } else if (val > 3000 && val <= 10000) {
            ret = val / 875d - 10d / 7d;
        } else if (val > 10000 && val <= 17000) {
            ret = val / 175d - 330d / 7d;
        } else if (val > 17000 && val <= 22000) {
            ret = val / 50d - 290d;
        } else if (val > 22000 && val <= 25500) {
            ret = 2d * val / 35d - 7750d / 7d;
        } else if (val > 25500 && val <= 28500) {
            ret = 7d * val / 60d - 2625d;
        } else if (val > 28500 && val <= 33000) {
            ret = 13d * val / 45d - 22600d / 3d;
        } else if (val > 33000 && val <= 40000) {
            ret = 8d * val / 7d - 250000d / 7d;
        } else if (val > 40000) {
            ret = 10d * val - 390000d;
        }
        return new BigDecimal(ret).setScale(2, RoundingMode.HALF_EVEN);
    }

    public static class LightLevel implements Comparable<LightLevel> {

        private final int level;
        private final boolean dark;
        private final boolean dayLight;

        public LightLevel(int level, boolean dark, boolean dayLight) {
            this.level = level;
            this.dark = dark;
            this.dayLight = dayLight;
        }

        public int getLevel() {
            return level;
        }

        public boolean isDark() {
            return dark;
        }

        public boolean isDayLight() {
            return dayLight;
        }

        @Override
        public int compareTo(LightLevel o) {
            return level - o.level;
        }

    }

}
src/main/java/lu/dainesch/huesense/hue/HueComm.java
public class HueComm {

    private static final Logger LOG = LoggerFactory.getLogger(HueComm.class);

    private static final String GET = "GET";
    private static final String POST = "POST";
    private static final String PUT = "PUT";

    private static final String PROTOCOL = "http://";
    private static final String DESCRIPTION = "/description.xml";
    private static final String API = "/api";
    private static final String SENSORS = "/sensors";
    private static final String IDENTBODY = "{\"devicetype\":\"huesense#app\"}";

    private final StringProperty bridgeIp = new SimpleStringProperty();
    private final BooleanProperty found = new SimpleBooleanProperty(false);
    private final BooleanProperty connected = new SimpleBooleanProperty(false);
    private final BooleanProperty authenticated = new SimpleBooleanProperty(false);

    private final ExecutorService execServ;
    private final HueSenseConfig config;
    private final DataStore store;

    private final Bridge bridge;

    public HueComm(HueSenseConfig config, DataStore store) {
        this.config = config;
        this.store = store;
        this.bridge = new Bridge(config.getString(Constants.BRIDGEIP), config.getString(Constants.BRIDGEKEY));
        this.bridgeIp.setValue(bridge.getIp());
        this.execServ = Executors.newFixedThreadPool(1, (Runnable r) -> {
            Thread ret = new Thread(r);
            ret.setDaemon(true);
            ret.setName(HueComm.class.getSimpleName() + " Thread");
            return ret;
        });

    }

    public void updateSensors() {
        if (!authenticated.get()) {
            return;
        }
        execServ.submit(() -> {
            try {
                String data = makeQuery(GET, PROTOCOL + bridge.getIp() + API + "/" + bridge.getKey() + SENSORS, null);
                store.updateSensorData(data);
            } catch (Exception ex) {
                LOG.error("Error updating sensors", ex);
            }
        });
    }

    public void startConnecting() {
        if (authenticated.get()) {
            return;
        }
        execServ.submit(() -> {

            try {
                if (testConnection() && testAuth()) {
                    // done
                    LOG.info("Using saved settings");
                    return;
                }

                bridge.setIp(UPNPUtil.searchBridge());

                if (bridge.getIp() != null && testConnection()) {
                    if (!testAuth()) {
                        waitForAuth();
                    }
                }

            } catch (Exception ex) {
                LOG.error("Error connecting", ex);
            }

        });

    }

    private boolean waitForAuth() throws InterruptedException, IOException {
        for (int i = 0; i < 100; i++) {

            if (getKey() && testAuth()) {
                return true;
            }
            Thread.sleep(3000);
        }
        return false;
    }

    private boolean getKey() throws IOException {
        String resp = makeQuery(POST, PROTOCOL + bridge.getIp() + API, IDENTBODY);
        if (resp != null) {
            JsonReader reader = Json.createReader(new StringReader(resp));
            JsonArray arr = reader.readArray();
            JsonObject obj = arr.getJsonObject(0);
            if (obj.containsKey("success")) {
                JsonObject success = obj.getJsonObject("success");
                bridge.setKey(success.getString("username"));
                LOG.info("Got key from bridge");
                return true;
            }

        }
        return false;
    }

    private boolean testAuth() throws IOException {
        if (bridge.getIp() != null && !bridge.getIp().isEmpty()
                && bridge.getKey() != null && !bridge.getKey().isEmpty()) {

            if (makeQuery(GET, PROTOCOL + bridge.getIp() + API + "/" + bridge.getKey() + SENSORS, null) != null) {
                saveConfig();
                LOG.info("Authenticated on bridge");
                Platform.runLater(() -> {
                    authenticated.set(true);
                });
                return true;
            } else {
                bridge.setKey(null);
            }
        }
        return false;
    }

    private boolean testConnection() throws IOException {
        if (bridge.getIp() != null && !bridge.getIp().isEmpty()) {
            if (makeQuery(GET, PROTOCOL + bridge.getIp() + DESCRIPTION, null) != null) {
                // connected
                LOG.info("Connection established");
                Platform.runLater(() -> {
                    found.set(true);
                    connected.set(true);
                    bridgeIp.set(bridge.getIp());
                });
                return true;
            } else {
                bridge.setIp(null);
                Platform.runLater(() -> {
                    bridgeIp.set(null);
                });
            }

        }
        return false;
    }

    private String makeQuery(String method, String path, String body) throws IOException {

        StringBuilder result = new StringBuilder();
        URL url = new URL(path);
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        conn.setRequestMethod(method);
        conn.setReadTimeout(Constants.TIMEOUT.intValue());
        if ((method.equals(POST) || method.endsWith(PUT)) && body != null) {
            conn.setDoOutput(true);
            try (BufferedWriter w = new BufferedWriter(new OutputStreamWriter(conn.getOutputStream(), StandardCharsets.UTF_8))) {
                w.write(body);
                w.flush();
            }
        }
        if (conn.getResponseCode() == 200) {
            try (BufferedReader rd = new BufferedReader(new InputStreamReader(conn.getInputStream()))) {
                String line;
                while ((line = rd.readLine()) != null) {
                    result.append(line);
                }
            }
            return result.toString();
        }
        return null;

    }

    public void renameSensor(int sensorId, String name) throws IOException {
        if (name == null || name.trim().isEmpty()) {
            return;
        }
        JsonObjectBuilder obj = Json.createObjectBuilder();
        obj.add("name", name);

        makeQuery(PUT, PROTOCOL + bridge.getIp() + API + "/" + bridge.getKey() + SENSORS + "/" + sensorId, obj.build().toString());

    }

    public StringProperty getBridgeIp() {
        return bridgeIp;
    }

    public BooleanProperty getFound() {
        return found;
    }

    public BooleanProperty getConnected() {
        return connected;
    }

    public BooleanProperty getAuthenticated() {
        return authenticated;
    }

    public HueSenseConfig getConfig() {
        return config;
    }

    private void saveConfig() {
        config.putString(Constants.BRIDGEIP, bridge.getIp());
        config.putString(Constants.BRIDGEKEY, bridge.getKey());
    }

}
src/main/java/lu/dainesch/huesense/view/mainscreen/sensors/PingSensorPresenter.java
public class PingSensorPresenter implements Initializable {

    @FXML
    private Label updateLabel;
    @FXML
    private Label pingLabel;
    @FXML
    private BorderPane contentPane;
    @FXML
    private ChoiceBox<GraphInterval> intervalCB;

    @FXML
    private CheckBox qvCB;

    private LineChart<Date, Number> lightChart;
    private PingSensor sensor;
    private SimpleDateFormat sdf;

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        sdf = new SimpleDateFormat(Constants.TIMEFORMAT);

        NumberAxis numberAxis = new NumberAxis();
        DateAxis dateAxis = new DateAxis();
        lightChart = new LineChart<>(dateAxis, numberAxis);
        contentPane.setCenter(lightChart);

        intervalCB.setItems(GraphInterval.INTERVALS);
        intervalCB.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> {
            if (sensor != null) {
                sensor.setGraphInterval(newValue);
            }
        });
        intervalCB.getSelectionModel().select(1);

    }

    public void setSensor(PingSensor sensor) {
        this.sensor = sensor;

        lightChart.setData(sensor.getData());
        updateData();

        sensor.getUpdateCount().addListener((observable, oldValue, newValue) -> {
            updateData();
        });
        qvCB.setSelected(sensor.getQuickView().get());
    }

    public void updateData() {
        if (sensor == null || sensor.getLastUpdate() == null) {
            return;
        }
        pingLabel.setText(sensor.getCurrentValueAsString());
        updateLabel.setText(sdf.format(sensor.getLastUpdate()));
    }

    @FXML
    void onQuickView(ActionEvent event) {
        sensor.getQuickView().set(!sensor.getQuickView().get());

    }

}
src/main/java/lu/dainesch/huesense/Constants.java
public class Constants {

    private Constants() {
    }

    public static final Long TIMEOUT = TimeUnit.SECONDS.toMillis(10);

    public static final String BRIDGEIP = "bridge";
    public static final String BRIDGEKEY = "bridgeKey";

    public static final String HUEDATEFORMAT = "yyyy-MM-dd'T'HH:mm:ss";
    public static final String TIMEFORMAT = "HH:mm:ss";
    public static final String DATEFORMAT = "dd MMM yyyy";

    public static final String QV_POS_X = "qvPos.x";
    public static final String QV_POS_Y = "qvPos.y";

    public static final String HIDE_ON_CLOSE = "hideOnClose";
    public static final String QV_COLOR = "qvColor";
    public static final String SHOW_QV = "qvShow";

    public static final String QV_SENSOR = "qvState.";
    public static final String TEMP_OFFSET_SENSOR = "temp.offset.";

    public static final String ENV_USER_HOME = System.getProperty("user.home");
    public static final String WORK_DIR = "/.huesense";
    public static final String DB_FILE = "/derby.db";

    public static final String JDBC = "jdbc:derby:" + ENV_USER_HOME + WORK_DIR + DB_FILE + ";create=true";
    
    public static final String CSV_SEP = ",";

}
src/main/java/lu/dainesch/huesense/view/mainscreen/sensors/TempSensorPresenter.java
public class TempSensorPresenter implements Initializable {

    private static final ObservableList<BigDecimal> OFFSET_VALUES = FXCollections.observableArrayList(
            new BigDecimal("-3"), new BigDecimal("-2.5"), new BigDecimal("-2"), new BigDecimal("-1.5"), new BigDecimal("-1"), new BigDecimal("-0.5"),
            new BigDecimal("0"),
            new BigDecimal("0.5"), new BigDecimal("1"), new BigDecimal("1.5"), new BigDecimal("2"), new BigDecimal("2.5"), new BigDecimal("3")
    );

    @FXML
    private RadioButton onRadio;
    @FXML
    private RadioButton reachRadio;
    @FXML
    private ProgressBar battProg;
    @FXML
    private TextField nameField;
    @FXML
    private Label updateLabel;
    @FXML
    private Label tempLabel;
    @FXML
    private BorderPane contentPane;
    @FXML
    private ChoiceBox<GraphInterval> intervalCB;
    @FXML
    private Button changeNameBut;
    @FXML
    private CheckBox qvCB;
    @FXML
    private ChoiceBox<BigDecimal> offsetCB;

    @Inject
    private HueComm hue;

    private LineChart<Date, Number> tempChart;
    private TempSensor sensor;
    private SimpleDateFormat sdf;
    private boolean isNameEdit;

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        sdf = new SimpleDateFormat(Constants.TIMEFORMAT);
        isNameEdit = false;

        NumberAxis numberAxis = new NumberAxis();
        numberAxis.setForceZeroInRange(false);
        DateAxis dateAxis = new DateAxis();
        tempChart = new LineChart<>(dateAxis, numberAxis);
        tempChart.setLegendVisible(false);
        tempChart.setCreateSymbols(false);
        contentPane.setCenter(tempChart);

        intervalCB.setItems(GraphInterval.INTERVALS);
        intervalCB.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> {
            if (sensor != null) {
                sensor.setGraphInterval(newValue);
            }
        });
        intervalCB.getSelectionModel().select(1);

        offsetCB.setItems(OFFSET_VALUES);
        offsetCB.getSelectionModel().select(6);

    }

    public void setSensor(TempSensor sensor) {
        this.sensor = sensor;

        tempChart.getData().add(sensor.getData());
        updateData();

        sensor.getUpdateCount().addListener((observable, oldValue, newValue) -> {
            updateData();
        });
        qvCB.setSelected(sensor.getQuickView().get());
        offsetCB.getSelectionModel().select(sensor.getTempOffset().get());
        sensor.getTempOffset().bind(offsetCB.getSelectionModel().selectedItemProperty());
    }

    public void updateData() {
        if (sensor == null) {
            return;
        }
        nameField.setText(sensor.getName());
        tempLabel.setText(sensor.getCurrentValueAsString());
        updateLabel.setText(sdf.format(sensor.getLastUpdate()));
        onRadio.setSelected(sensor.getOn());
        reachRadio.setSelected(sensor.getReachable());
        battProg.setProgress((double) sensor.getBattery() / 100d);
    }

    @FXML
    void onChangeName(ActionEvent event) {
        if (!isNameEdit) {
            nameField.setEditable(true);
            nameField.requestFocus();
            changeNameBut.setText("Save");
            isNameEdit = true;
        } else {
            try {
                // save
                hue.renameSensor(sensor.getId(), nameField.getText());
                hue.updateSensors();
                nameField.setEditable(false);
                changeNameBut.setText("Change");
                isNameEdit = false;
            } catch (IOException ex) {
                // ignore
            }
        }
    }

    @FXML
    void onQuickView(ActionEvent event) {
        sensor.getQuickView().set(!sensor.getQuickView().get());
    }

}
src/main/java/lu/dainesch/huesense/view/export/ExportView.java
public class ExportView extends FXMLView {
    
}
src/main/java/lu/dainesch/huesense/view/mainscreen/sensors/TempSensorView.java
public class TempSensorView extends FXMLView {
    
}
src/main/java/lu/dainesch/huesense/HueSenseConfig.java
public class HueSenseConfig {

    private final Preferences preferences;

    public HueSenseConfig() {
        preferences = Preferences.userNodeForPackage(HueSenseConfig.class);
    }

    public Object getInjectionValue(Object key) {
        return preferences.get((String) key, null);
    }

    public void putDouble(String key, double val) {
        preferences.putDouble(key, val);
    }

    public double getDouble(String key) {
        return preferences.getDouble(key, -1);
    }

    public void putInt(String key, int value) {
        preferences.putInt(key, value);
    }

    public int getInt(String key) {
        return preferences.getInt(key, -1);
    }

    public void putBoolean(String key, boolean val) {
        preferences.putBoolean(key, val);
    }

    public boolean getBoolean(String key) {
        return preferences.getBoolean(key, false);
    }

    public void putString(String key, String val) {
        preferences.put(key, val);
    }

    public String getString(String key) {
        return preferences.get(key, null);
    }

}
src/main/java/lu/dainesch/huesense/view/quickview/QuickPresenter.java
public class QuickPresenter implements Initializable {

    @FXML
    private VBox vBox;
    @FXML
    private Rectangle moveRect;

    @Inject
    private DataStore store;
    @Inject
    private HueSenseConfig config;

    private InvisibleFrame frame;
    private double xOffset = 0;
    private double yOffset = 0;

    private final ObjectProperty<Color> color = new SimpleObjectProperty<>(Color.WHITE);
    private final Map<Integer, Node> sensors = new HashMap<>();

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        frame = new InvisibleFrame();
        vBox.setBackground(null);
        Scene scene = new Scene(vBox);
        scene.setFill(Color.TRANSPARENT);
        frame.setScene(scene);

        store.getSensors().forEach((s) -> addSensor(s));

        store.getSensors().addListener((ListChangeListener.Change<? extends Sensor<?>> c) -> {
            while (c.next()) {
                c.getAddedSubList().forEach((s) -> {
                    addSensor(s);
                });
                c.getRemoved().forEach((s) -> {
                    vBox.getChildren().remove(sensors.get(s.getId()));
                    sensors.remove(s.getId());
                });
            }
        });

        color.addListener((observable, oldValue, newValue) -> {
            moveRect.setFill(newValue);
            JsonObjectBuilder o = Json.createObjectBuilder();
            o.add("r", newValue.getRed());
            o.add("g", newValue.getGreen());
            o.add("b", newValue.getBlue());
            o.add("a", newValue.getOpacity());
            config.putString(Constants.QV_COLOR, o.build().toString());

        });
        String col = config.getString(Constants.QV_COLOR);
        if (col != null && !col.isEmpty()) {
            JsonObject o = Json.createReader(new StringReader(col)).readObject();
            color.set(new Color(o.getJsonNumber("r").doubleValue(),
                    o.getJsonNumber("g").doubleValue(),
                    o.getJsonNumber("b").doubleValue(),
                    o.getJsonNumber("a").doubleValue()));
        }

        if (config.getInt(Constants.QV_POS_X) > 0 && config.getInt(Constants.QV_POS_Y) > 0) {
            frame.setLocation(config.getInt(Constants.QV_POS_X), config.getInt(Constants.QV_POS_Y));
        }

    }

    private void addSensor(Sensor<?> sensor) {
        if (!sensors.containsKey(sensor.getId())) {
            QuickItemView qiv = new QuickItemView();
            QuickItemPresenter qip = (QuickItemPresenter) qiv.getPresenter();
            qip.setItem(sensor, color);
            Node v = qiv.getView();
            sensors.put(sensor.getId(), v);
            if (sensor.getQuickView().get()) {
                vBox.getChildren().add(v);
            }

        }
        sensor.getQuickView().addListener((observable, oldValue, newValue) -> {
            if (newValue) {
                vBox.getChildren().add(sensors.get(sensor.getId()));
            } else {
                vBox.getChildren().remove(sensors.get(sensor.getId()));
            }
        });

    }

    public void setVisible(boolean visible) {
        frame.setVisible(visible);
    }

    @FXML
    void moveMouseDragged(MouseEvent event) {
        frame.setLocation((int) (event.getScreenX() - xOffset), (int) (event.getScreenY() - yOffset));

    }

    @FXML
    void moveMousePressed(MouseEvent event) {
        xOffset = event.getSceneX();
        yOffset = event.getSceneY();
    }

    @FXML
    void moveMouseReleased(MouseEvent event) {
        Point p = frame.getLocation();
        config.putInt(Constants.QV_POS_X, p.x);
        config.putInt(Constants.QV_POS_Y, p.y);
    }

    public ObjectProperty<Color> getColor() {
        return color;
    }

    private static final class InvisibleFrame extends JFrame {

        private final JFXPanel fxPanel;

        public InvisibleFrame() {
            setType(Type.UTILITY);
            setUndecorated(true);
            setAlwaysOnTop(true);

            setBackground(new java.awt.Color(0, 0, 0, 0));
            setDefaultCloseOperation(JFrame.HIDE_ON_CLOSE);

            setLocationRelativeTo(null);
            setSize(200, 1000);

            fxPanel = new JFXPanel();
            getContentPane().add(fxPanel);

        }

        public void setScene(Scene scene) {
            fxPanel.setScene(scene);
        }

        @Override
        public void setLocation(int x, int y) {
            SwingUtilities.invokeLater(() -> {
                super.setLocation(x, y);
            });

        }

        @Override
        public void setVisible(boolean visible) {
            SwingUtilities.invokeLater(() -> {
                super.setVisible(visible);
            });
        }

    }

}
src/main/java/lu/dainesch/huesense/view/UIUtils.java
public class UIUtils {

    private UIUtils() {
    }

    public static void setIcon(Dialog diag) {
        Stage stage = (Stage) diag.getDialogPane().getScene().getWindow();
        setIcon(stage);
    }

    public static Image getImage(String path) {
        return new Image( path);
    }

    public static void setIcon(Stage stage) {
        stage.getIcons().add(getImage("icon.png"));
    }

    public static <S> void addAutoScroll(final TableView<S> view) {
        if (view == null) {
            throw new NullPointerException();
        }

        view.getItems().addListener((ListChangeListener<S>) (c -> {
            c.next();
            final int size = view.getItems().size();
            if (size > 0) {
                view.scrollTo(size - 1);
            }
        }));
    }

    public static <S> void addAutoScroll(final ListView<S> view) {
        if (view == null) {
            throw new NullPointerException();
        }

        view.getItems().addListener((ListChangeListener<S>) (c -> {
            c.next();
            final int size = view.getItems().size();
            if (size > 0) {
                view.scrollTo(size - 1);
            }
        }));
    }

    public static void setTextFieldNumbersOnly(TextField field, int min, int max) {
        field.addEventFilter(KeyEvent.KEY_TYPED, (e) -> {

            if (e.getCharacter().matches("[0-9]")) {
                String text = field.getText();
                if (e.getCode() == KeyCode.BACK_SPACE && text.length() > 0) {
                    text = text.substring(0, text.length() - 2);
                } else {
                    text = text + e.getCharacter();
                }
                try {
                    long value = Long.parseLong(text);
                    if (value < min) {
                        e.consume();
                        field.setText(String.valueOf(min));
                    } else if (value > max) {
                        e.consume();
                        field.setText(String.valueOf(max));
                    }
                } catch (NumberFormatException ex) {
                    e.consume();
                }
            } else {
                e.consume();
            }
        });
    }

}
src/main/java/lu/dainesch/huesense/hue/data/PresenceSensor.java
public class PresenceSensor extends Sensor<Boolean> {

    private static final String INSERT_CUR_VAL = "Insert into PRESENCE_DATA (SD_ID, PRESENCE) values (?, ?)";
    private static final String SELECT_RANGE = "Select p.SD_ID, p.PRESENCE, d.CREATED from PRESENCE_DATA p "
            + "JOIN SENSOR_DATA d on d.SD_ID=p.SD_ID "
            + "where d.SENSOR_ID = ? and d.CREATED > ? and d.CREATED <= ? "
            + "order by d.created asc ";

    private static final String DETECT = "Motion detected";
    private static final String NOTHING = "No motion";
    private static final Logger LOG = LoggerFactory.getLogger(PresenceSensor.class);

    private final ObservableList<PresenceEntry> data;
    private final ObjectProperty<GraphInterval> graphInterval;
    private PresenceEntry lastEntry = null;
    private boolean currentDetect = false;

    public PresenceSensor(int id, String uniqueID, HueSenseConfig config, DBManager dbMan) {
        super(id, uniqueID, SensorType.PRESENCE, config, dbMan);
        data = FXCollections.observableArrayList();
        graphInterval = new SimpleObjectProperty<>(GraphInterval.INTERVALS.get(1));
    }

    @Override
    public void initSensor() {
        // load data
        setGraphInterval(GraphInterval.INTERVALS.get(1));
    }

    @Override
    public void updateSensor(JsonObject obj) throws UpdateException {
        try {
            setName(obj.getString("name"));
            JsonObject state = obj.getJsonObject("state");

            Boolean pres = state.getBoolean("presence");

            SimpleDateFormat sdf = new SimpleDateFormat(Constants.HUEDATEFORMAT);
            sdf.setTimeZone(TimeZone.getTimeZone("GMT"));
            Date time = sdf.parse(state.getString("lastupdated"));
            SensorValue<Boolean> val = new SensorValue<>(time, pres);
            updateValue(val);

            JsonObject conf = obj.getJsonObject("config");
            setOn(conf.getBoolean("on"));
            setBattery(conf.getInt("battery"));
            setReachable(conf.getBoolean("reachable"));

        } catch (ParseException | NullPointerException | ClassCastException ex) {
            throw new UpdateException("Error updating temp sensor", ex);
        }
    }

    @Override
    public void saveCurrentValueInDB(Long dataId) throws UpdateException {
        try (Connection conn = dbMan.getConnection()) {
            try (PreparedStatement stmt = conn.prepareStatement(INSERT_CUR_VAL)) {
                stmt.setLong(1, dataId);
                stmt.setBoolean(2, currentValue);
                stmt.executeUpdate();
            }
        } catch (SQLException ex) {
            throw new UpdateException("Error updating presence data", ex);
        }
    }

    @Override
    public Set<SensorValue<Boolean>> getValuesInRange(Date start, Date end) {
        NavigableSet<SensorValue<Boolean>> ret = new TreeSet<>();

        try (Connection conn = dbMan.getConnection()) {
            try (PreparedStatement stmt = conn.prepareStatement(SELECT_RANGE)) {
                stmt.setLong(1, dbId);
                stmt.setTimestamp(2, new Timestamp(start.getTime()));
                long endTime = end == null ? System.currentTimeMillis() : end.getTime();
                stmt.setTimestamp(3, new Timestamp(endTime));
                ResultSet rs = stmt.executeQuery();
                while (rs.next()) {
                    SensorValue<Boolean> val = new SensorValue<>(rs.getTimestamp("CREATED"), rs.getBoolean("PRESENCE"));
                    ret.add(val);
                }
            }
        } catch (SQLException ex) {
            LOG.error("Error querying light values", ex);
        }
        return ret;
    }

    @Override
    public void updateView(SensorValue<Boolean> val) {
        Duration maxAge = graphInterval.get().getDuration();
        while (!data.isEmpty() && (System.currentTimeMillis() - data.get(data.size() - 1).getStart().getTime() > maxAge.toMillis())) {
            // remove old values
            data.remove(data.size() - 1);
        }
        if (val == null || System.currentTimeMillis() - val.getTime().getTime() > maxAge.toMillis()) {
            // do not add old data
            return;
        }

        boolean detect = val.getValue();

        if (lastEntry == null) {
            if (detect) {
                // first entry
                lastEntry = new PresenceEntry(val.getTime());
                data.add(0, lastEntry);
                currentDetect = true;
            } else {
                // nothing
                currentDetect = false;
            }
            return;
        }

        if (detect) {
            if (!currentDetect) {
                // new detection
                lastEntry = new PresenceEntry(val.getTime());
                data.add(0, lastEntry);
            }
            currentDetect = true;
        } else {
            if (currentDetect && lastEntry != null) {
                // end detection
                lastEntry.setEnd(val.getTime());
            }
            currentDetect = false;
        }

    }

    public ObservableList<PresenceEntry> getData() {
        return data;
    }

    public void setGraphInterval(GraphInterval val) {

        Date start = new Date(System.currentTimeMillis() - (long) val.getDuration().toMillis());
        data.removeAll(data);   // clear does not work
        graphInterval.set(val);
        getValuesInRange(start, null).forEach((s) -> {
            updateView(s);
        });

    }

    @Override
    public String getValueAsString(Boolean value) {
        if (value) {
            return DETECT;
        }
        return NOTHING;
    }

    @Override
    public String[] getCSVHeader() {
        return new String[]{"Time", "Presence"};
    }

    @Override
    public String[] getCSVEntry(SensorValue<Boolean> val, SimpleDateFormat sdf) {
        return new String[]{sdf.format(val.getTime()), val.getValue().toString()};
    }

    public static class PresenceEntry {

        private final SimpleStringProperty date = new SimpleStringProperty();
        private final SimpleStringProperty startTime = new SimpleStringProperty();
        private final SimpleStringProperty endTime = new SimpleStringProperty(null);
        private final SimpleStringProperty duration = new SimpleStringProperty(null);

        private final Date start;

        public PresenceEntry(Date start) {
            this.start = start;
            date.set(new SimpleDateFormat(Constants.DATEFORMAT).format(start));
            startTime.set(new SimpleDateFormat(Constants.TIMEFORMAT).format(start));

        }

        public void setEnd(Date end) {
            endTime.set(new SimpleDateFormat(Constants.TIMEFORMAT).format(end));
            Duration d = Duration.millis(end.getTime() - start.getTime());
            duration.set(((int) d.toSeconds()) + " seconds");
        }

        public Date getStart() {
            return start;
        }

        public String getDate() {
            return date.get();
        }

        public void setDate(String date) {
            this.date.set(date);
        }

        public String getStartTime() {
            return startTime.get();
        }

        public void setStartTime(String startTime) {
            this.startTime.set(startTime);
        }

        public String getEndTime() {
            return endTime.get();
        }

        public void setEndTime(String endTime) {
            this.endTime.set(endTime);
        }

        public String getDuration() {
            return duration.get();
        }

        public void setDuration(String duration) {
            this.duration.set(duration);
        }

        public SimpleStringProperty dateProperty() {
            return date;
        }

        public SimpleStringProperty startTimeProperty() {
            return startTime;
        }

        public SimpleStringProperty endTimeProperty() {
            return endTime;
        }

        public SimpleStringProperty durationProperty() {
            return duration;
        }

    }

}
src/main/java/lu/dainesch/huesense/hue/data/TempSensor.java
public class TempSensor extends Sensor<BigDecimal> {

    private static final String INSERT_CUR_VAL = "Insert into TEMP_DATA (SD_ID, TEMPERATURE) values (?, ?)";
    private static final String SELECT_RANGE = "Select t.SD_ID, t.TEMPERATURE, d.CREATED from TEMP_DATA t "
            + "JOIN SENSOR_DATA d on d.SD_ID=t.SD_ID "
            + "where d.SENSOR_ID = ? and d.CREATED > ? and d.CREATED <= ? "
            + "order by d.created asc ";
    public static final String TEMP_SUFFIX = " °C";
    private static final Logger LOG = LoggerFactory.getLogger(TempSensor.class);

    private final XYChart.Series<Date, Number> data;
    private final ObjectProperty<GraphInterval> graphInterval;
    private final ObjectProperty<BigDecimal> tempOffset;

    public TempSensor(int id, String uniqueID, HueSenseConfig config, DBManager dbMan) {
        super(id, uniqueID, SensorType.TEMPERATURE, config, dbMan);
        data = new XYChart.Series<>();
        data.setName("Temperature");
        graphInterval = new SimpleObjectProperty<>(GraphInterval.INTERVALS.get(1));
        tempOffset = new SimpleObjectProperty<>(BigDecimal.ZERO);
        if (config.getString(Constants.TEMP_OFFSET_SENSOR + id) != null) {
            tempOffset.set(new BigDecimal(config.getString(Constants.TEMP_OFFSET_SENSOR + id)));
        }
        tempOffset.addListener((observable, oldValue, newValue) -> {
            changeTempOffset();
        });

    }

    @Override
    public void initSensor() {
        // load data
        setGraphInterval(GraphInterval.INTERVALS.get(1));
    }

    @Override
    public void updateSensor(JsonObject obj) throws UpdateException {
        try {
            setName(obj.getString("name"));

            JsonObject state = obj.getJsonObject("state");

            int t = state.getInt("temperature");
            BigDecimal temp = new BigDecimal(t).movePointLeft(2);

            SimpleDateFormat sdf = new SimpleDateFormat(Constants.HUEDATEFORMAT);
            sdf.setTimeZone(TimeZone.getTimeZone("GMT"));
            Date time = sdf.parse(state.getString("lastupdated"));

            SensorValue<BigDecimal> val = new SensorValue<>(time, temp);
            updateValue(val);

            JsonObject conf = obj.getJsonObject("config");
            setOn(conf.getBoolean("on"));
            setBattery(conf.getInt("battery"));
            setReachable(conf.getBoolean("reachable"));

        } catch (ParseException | NullPointerException | ClassCastException ex) {
            throw new UpdateException("Error updating temp sensor", ex);
        }
    }

    @Override
    public synchronized BigDecimal getCurrentValue() {
        return super.getCurrentValue().add(tempOffset.get());
    }

    @Override
    public void saveCurrentValueInDB(Long dataId) throws UpdateException {
        try (Connection conn = dbMan.getConnection()) {
            try (PreparedStatement stmt = conn.prepareStatement(INSERT_CUR_VAL)) {
                stmt.setLong(1, dataId);
                stmt.setBigDecimal(2, currentValue);
                stmt.executeUpdate();
            }
        } catch (SQLException ex) {
            throw new UpdateException("Error updating presence data", ex);
        }
    }

    @Override
    public Set<SensorValue<BigDecimal>> getValuesInRange(Date start, Date end) {
        NavigableSet<SensorValue<BigDecimal>> ret = new TreeSet<>();

        try (Connection conn = dbMan.getConnection()) {
            try (PreparedStatement stmt = conn.prepareStatement(SELECT_RANGE)) {
                stmt.setLong(1, dbId);
                stmt.setTimestamp(2, new Timestamp(start.getTime()));
                long endTime = end == null ? System.currentTimeMillis() : end.getTime();
                stmt.setTimestamp(3, new Timestamp(endTime));
                ResultSet rs = stmt.executeQuery();
                while (rs.next()) {
                    SensorValue<BigDecimal> val = new SensorValue<>(rs.getTimestamp("CREATED"), rs.getBigDecimal("TEMPERATURE"));
                    ret.add(val);
                }
            }
        } catch (SQLException ex) {
            LOG.error("Error querying temp values", ex);
        }
        return ret;
    }

    @Override
    public void updateView(SensorValue<BigDecimal> val) {
        Duration maxAge = graphInterval.get().getDuration();
        while (!data.getData().isEmpty() && (System.currentTimeMillis() - data.getData().get(0).getXValue().getTime() > maxAge.toMillis())) {
            // remove old values
            data.getData().remove(0);
        }
        if (val == null || System.currentTimeMillis() - val.getTime().getTime() > maxAge.toMillis()) {
            // do not add old data
            return;
        }

        data.getData().add(new XYChart.Data<>(val.getTime(), val.getValue().add(tempOffset.get()).doubleValue()));
    }

    public XYChart.Series<Date, Number> getData() {
        return data;
    }

    @Override
    public String getValueAsString(BigDecimal value) {
        return value.add(tempOffset.get()).toPlainString() + TEMP_SUFFIX;
    }

    public void setGraphInterval(GraphInterval val) {

        Date start = new Date(System.currentTimeMillis() - (long) val.getDuration().toMillis());
        data.getData().removeAll(data.getData());   // clear does not work
        graphInterval.set(val);
        getValuesInRange(start, null).forEach((s) -> {
            updateView(s);
        });

    }

    private void changeTempOffset() {
        // redraw
        Date start = new Date(System.currentTimeMillis() - (long) graphInterval.get().getDuration().toMillis());
        data.getData().removeAll(data.getData());   // clear does not work
        getValuesInRange(start, null).forEach((s) -> {
            updateView(s);
        });
        // trigger change, not elegant, but meh
        updateCount.set(updateCount.get() - 1);
        updateCount.set(updateCount.get() + 1);
        config.putString(Constants.TEMP_OFFSET_SENSOR + id, tempOffset.get().toPlainString());
    }

    public ObjectProperty<BigDecimal> getTempOffset() {
        return tempOffset;
    }

    @Override
    public String[] getCSVHeader() {
        return new String[]{"Time", "Temperature"};
    }

    @Override
    public String[] getCSVEntry(SensorValue<BigDecimal> val, SimpleDateFormat sdf) {
        return new String[]{sdf.format(val.getTime()), val.getValue().toString()};
    }

}
src/main/java/lu/dainesch/huesense/view/mainscreen/sensors/LightSensorPresenter.java
public class LightSensorPresenter implements Initializable {

    @FXML
    private RadioButton onRadio;
    @FXML
    private RadioButton reachRadio;
    @FXML
    private ProgressBar battProg;
    @FXML
    private TextField nameField;
    @FXML
    private Label updateLabel;
    @FXML
    private Label lightLabel;
    @FXML
    private BorderPane contentPane;
    @FXML
    private ChoiceBox<GraphInterval> intervalCB;
    @FXML
    private Button changeNameBut;
    @FXML
    private CheckBox qvCB;

    @Inject
    private HueComm hue;

    private LineChart<Date, Number> lightChart;
    private LightSensor sensor;
    private SimpleDateFormat sdf;
    private boolean isNameEdit;

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        sdf = new SimpleDateFormat(Constants.TIMEFORMAT);

        NumberAxis numberAxis = new NumberAxis();
        DateAxis dateAxis = new DateAxis();
        lightChart = new LineChart<>(dateAxis, numberAxis);
        lightChart.setLegendVisible(false);
        lightChart.setCreateSymbols(false);
        contentPane.setCenter(lightChart);

        intervalCB.setItems(GraphInterval.INTERVALS);
        intervalCB.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> {
            if (sensor != null) {
                sensor.setGraphInterval(newValue);
            }
        });
        intervalCB.getSelectionModel().select(1);

    }

    public void setSensor(LightSensor sensor) {
        this.sensor = sensor;

        lightChart.getData().add(sensor.getData());
        updateData();

        sensor.getUpdateCount().addListener((observable, oldValue, newValue) -> {
            updateData();
        });
        qvCB.setSelected(sensor.getQuickView().get());
    }

    public void updateData() {
        if (sensor == null) {
            return;
        }
        nameField.setText(sensor.getName());
        lightLabel.setText(sensor.getCurrentValueAsString());
        updateLabel.setText(sdf.format(sensor.getLastUpdate()));
        onRadio.setSelected(sensor.getOn());
        reachRadio.setSelected(sensor.getReachable());
        battProg.setProgress((double) sensor.getBattery() / 100d);
    }

    @FXML
    void onChangeName(ActionEvent event) {
        if (!isNameEdit) {
            nameField.setEditable(true);
            nameField.requestFocus();
            changeNameBut.setText("Save");
            isNameEdit = true;
        } else {
            try {
                // save
                hue.renameSensor(sensor.getId(), nameField.getText());
                hue.updateSensors();
                nameField.setEditable(false);
                changeNameBut.setText("Change");
                isNameEdit = false;
            } catch (IOException ex) {
                // ignore
            }
        }
    }

    @FXML
    void onQuickView(ActionEvent event) {
        sensor.getQuickView().set(!sensor.getQuickView().get());
        
    }

}
src/main/java/lu/dainesch/huesense/hue/data/PingSensor.java
public class PingSensor extends Sensor<PingSensor.PingValues> {

    private static final String INSERT_CUR_VAL = "Insert into PING_DATA (SD_ID, LATENCY) values (?, ?)";

    private final ObservableList<XYChart.Series<Date, Number>> data;
    private final ObjectProperty<GraphInterval> graphInterval;
    private final Map<String, XYChart.Series<Date, Number>> seriesMap;
    private boolean lastdetected;

    public PingSensor(int id, String uniqueID, HueSenseConfig config, DBManager dbMan) {
        super(id, uniqueID, SensorType.PING, config, dbMan);
        graphInterval = new SimpleObjectProperty<>(GraphInterval.INTERVALS.get(1));
        data = FXCollections.observableArrayList();
        seriesMap = new HashMap<>();
    }

    @Override
    public void initSensor() {
        // load data
        setGraphInterval(GraphInterval.INTERVALS.get(1));
    }

    public void updateSensor(Map<String, Integer> values) {
        SensorValue<PingValues> val = new SensorValue<>(new Date(), new PingValues(values));

        long count = values.entrySet().stream().map(e -> e.getValue()).filter(e -> e > 0).collect(Collectors.counting());
        if (count > 0) {
            updateValue(val);
            lastdetected = true;
        } else if (lastdetected) {
            updateValue(val);
            lastdetected = false;
        }

    }

    @Override
    public void updateSensor(JsonObject obj) throws UpdateException {

    }

    @Override
    public void saveCurrentValueInDB(Long dataId) throws UpdateException {
        try (Connection conn = dbMan.getConnection()) {
            try (PreparedStatement stmt = conn.prepareStatement(INSERT_CUR_VAL)) {
                stmt.setLong(1, dataId);
                stmt.setInt(2, currentValue.getMaxValue());
                stmt.executeUpdate();
            }
        } catch (SQLException ex) {
            throw new UpdateException("Error updating presence data", ex);
        }
    }

    @Override
    public Set<SensorValue<PingValues>> getValuesInRange(Date start, Date end) {
        NavigableSet<SensorValue<PingValues>> ret = new TreeSet<>();

        return ret;
    }

    @Override
    public void updateView(SensorValue<PingValues> val) {
        Duration maxAge = graphInterval.get().getDuration();
        for (XYChart.Series<Date, Number> ser : data) {
            while (!ser.getData().isEmpty() && (System.currentTimeMillis() - ser.getData().get(0).getXValue().getTime() > maxAge.toMillis())) {
                // remove old values
                ser.getData().remove(0);
            }
        }
        if (val == null || System.currentTimeMillis() - val.getTime().getTime() > maxAge.toMillis()) {
            // do not add old data
            return;
        }
        for (Entry<String, Integer> e : val.getValue().getValues().entrySet()) {
            XYChart.Series<Date, Number> ser;
            if (!seriesMap.containsKey(e.getKey())) {
                ser = new XYChart.Series<>();
                ser.setName(e.getKey());
                seriesMap.put(e.getKey(), ser);
                data.add(ser);
            }
            ser = seriesMap.get(e.getKey());
            ser.getData().add(new XYChart.Data<>(val.getTime(), e.getValue()));
        }

    }

    public ObservableList<XYChart.Series<Date, Number>> getData() {
        return data;
    }

    @Override
    public String getValueAsString(PingValues value) {
        long count = value.getValues().entrySet().stream().map(e -> e.getValue()).filter(e -> e > 0).collect(Collectors.counting());
        if (count > 0) {
            return count + " device(s) present";
        }
        return "No device present";
    }
    
    @Override
    public String[] getCSVHeader() {
        return new String[]{"Time", ""};
    }

    @Override
    public String[] getCSVEntry(SensorValue<PingValues> val, SimpleDateFormat sdf) {
        return new String[]{sdf.format(val.getTime()), val.getValue().toString()};
    }

    public void setGraphInterval(GraphInterval val) {

        Date start = new Date(System.currentTimeMillis() - (long) val.getDuration().toMillis());
        for (XYChart.Series<Date, Number> ser : data) {
            ser.getData().removeAll(ser.getData());   // clear does not work
        }
        graphInterval.set(val);
        getValuesInRange(start, null).forEach((s) -> {
            updateView(s);
        });

    }

    public static class PingValues {

        private final Map<String, Integer> values;

        public PingValues(Map<String, Integer> values) {
            this.values = values;
        }

        public void addValue(String device, int ping) {
            values.put(device, ping);
        }

        public Map<String, Integer> getValues() {
            return values;
        }

        public Integer getMaxValue() {

            Optional<Integer> max = values.entrySet().stream().map(e -> e.getValue()).max((a, b) -> a - b);
            if (max.isPresent()) {
                return max.get();
            }
            return 0;
        }

    }

}
src/main/java/lu/dainesch/huesense/view/mainscreen/sensors/LightSensorView.java
public class LightSensorView extends FXMLView {
    
}
src/main/java/lu/dainesch/huesense/view/about/AboutView.java
public class AboutView extends FXMLView {
    
    
}
src/main/java/lu/dainesch/huesense/view/mainscreen/sensors/PresenceSensorPresenter.java
public class PresenceSensorPresenter implements Initializable {

    @FXML
    private RadioButton onRadio;
    @FXML
    private RadioButton reachRadio;
    @FXML
    private ProgressBar battProg;
    @FXML
    private TextField nameField;
    @FXML
    private Label updateLabel;
    @FXML
    private Label presenceLabel;
    @FXML
    private ChoiceBox<GraphInterval> intervalCB;
    @FXML
    private Button changeNameBut;
    @FXML
    private TableView<PresenceEntry> presTable;
    @FXML
    private CheckBox qvCB;

    @Inject
    private HueComm hue;

    private PresenceSensor sensor;
    private SimpleDateFormat sdf;
    private boolean isNameEdit;

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        sdf = new SimpleDateFormat(Constants.TIMEFORMAT);

        TableColumn dateCol = new TableColumn("Date");
        TableColumn startCol = new TableColumn("Start");
        TableColumn endCol = new TableColumn("End");
        TableColumn durCol = new TableColumn("Duration");

        dateCol.setCellValueFactory(
                new PropertyValueFactory<>("date")
        );
        startCol.setCellValueFactory(
                new PropertyValueFactory<>("startTime")
        );
        endCol.setCellValueFactory(
                new PropertyValueFactory<>("endTime")
        );
        durCol.setCellValueFactory(
                new PropertyValueFactory<>("duration")
        );
        presTable.getColumns().addAll(dateCol, startCol, endCol, durCol);

        intervalCB.setItems(GraphInterval.INTERVALS);
        intervalCB.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> {
            if (sensor != null) {
                sensor.setGraphInterval(newValue);
            }
        });
        intervalCB.getSelectionModel().select(1);

    }

    public void setSensor(PresenceSensor sensor) {
        this.sensor = sensor;

        presTable.setItems(sensor.getData());
        updateData();

        sensor.getUpdateCount().addListener((observable, oldValue, newValue) -> {
            updateData();
        });
        qvCB.setSelected(sensor.getQuickView().get());
    }

    public void updateData() {
        if (sensor == null) {
            return;
        }
        nameField.setText(sensor.getName());
        presenceLabel.setText(sensor.getCurrentValueAsString());
        updateLabel.setText(sdf.format(sensor.getLastUpdate()));
        onRadio.setSelected(sensor.getOn());
        reachRadio.setSelected(sensor.getReachable());
        battProg.setProgress((double) sensor.getBattery() / 100d);
    }

    @FXML
    void onChangeName(ActionEvent event) {
        if (!isNameEdit) {
            nameField.setEditable(true);
            nameField.requestFocus();
            changeNameBut.setText("Save");
            isNameEdit = true;
        } else {
            try {
                // save
                hue.renameSensor(sensor.getId(), nameField.getText());
                hue.updateSensors();
                nameField.setEditable(false);
                changeNameBut.setText("Change");
                isNameEdit = false;
            } catch (IOException ex) {
                // ignore
            }
        }
    }

    @FXML
    void onQuickView(ActionEvent event) {
        sensor.getQuickView().set(!sensor.getQuickView().get());
    }

}
src/main/java/lu/dainesch/huesense/view/mainscreen/sensors/PresenceSensorView.java
public class PresenceSensorView extends FXMLView {
    
}
src/main/java/lu/dainesch/huesense/view/alarm/AlarmConfigView.java
public class AlarmConfigView extends FXMLView {
    
}
src/main/java/lu/dainesch/huesense/hue/DataStore.java
public class DataStore {

    private static final Logger LOG = LoggerFactory.getLogger(DataStore.class);
    private static final String STMT_CREATE_ENTRY = "Insert into SENSOR_DATA (SENSOR_ID, CREATED, IS_ON, IS_REACHABLE, BATTERY_LEVEL)"
            + "values (?, ?, ?, ?, ?)";

    private final HueSenseConfig config;
    private final DBManager dbMan;

    private final Map<Integer, Sensor<?>> sensorMap = Collections.synchronizedMap(new HashMap<>());
    private final ObservableList<Sensor<?>> sensors = FXCollections.observableArrayList();

    public DataStore(HueSenseConfig config, DBManager dbMan) {
        this.config = config;
        this.dbMan = dbMan;
    }

    public void updateSensorData(String data) {
        JsonObject list = Json.createReader(new StringReader(data)).readObject();
        for (String sId : list.keySet()) {
            Integer id = Integer.parseInt(sId);

            JsonObject sensObj = list.getJsonObject(sId);
            String sType = sensObj.getString("type");
            SensorType type = SensorType.getByType(sType);

            if (type != null) {
                // only supported types

                Sensor<?> sensor = sensorMap.get(id);
                boolean isNew = false;
                if (sensor == null) {
                    // create it
                    String uniqueID = sensObj.getString("uniqueid");
                    sensor = createSensor(id, uniqueID, type);
                    isNew = true;
                }

                if (sensor != null) {
                    try {
                        sensor.updateSensor(sensObj);
                        if (isNew) {
                            addSensor(sensor);
                        }

                        // database save
                        Long entryId = createSensorDataEntry(sensor);
                        if (entryId != null) {
                            sensor.saveCurrentValueInDB(entryId);
                        }
                    } catch (UpdateException ex) {
                        LOG.error("Error updating sensor data", ex);
                    }
                }

            }

        }
    }

    public void addSensor(Sensor<?> sensor) throws UpdateException {
        addOrUpdateSensorDB(sensor);

        sensorMap.put(sensor.getId(), sensor);
        Platform.runLater(() -> {
            sensors.add(sensor);
            FXCollections.sort(sensors);
            sensor.initSensor();
        });
    }

    private Sensor<?> createSensor(Integer id, String uniqueID, SensorType type) {

        switch (type) {
            case LIGHT:
                return new LightSensor(id, uniqueID, config, dbMan);

            case PRESENCE:
                return new PresenceSensor(id, uniqueID, config, dbMan);

            case TEMPERATURE:
                return new TempSensor(id, uniqueID, config, dbMan);

            default:
                LOG.error("Unknown sensor type: " + type);

        }
        return null;
    }

    private void addOrUpdateSensorDB(Sensor<?> sensor) throws UpdateException {
        String name = null;
        try (Connection conn = dbMan.getConnection()) {
            try (PreparedStatement stmt = conn.prepareStatement(Sensor.STMT_SEL_UID)) {
                stmt.setString(1, sensor.getUniqueID());
                ResultSet rs = stmt.executeQuery();
                while (rs.next()) {
                    name = rs.getString("SENSOR_NAME");
                    if (sensor.getDbId() == null) {
                        sensor.setDbId(rs.getLong("S_ID"));
                    }
                }
            }

            if (sensor.getDbId() == null) {
                // create new sensor
                try (PreparedStatement stmt = conn.prepareStatement(Sensor.STMT_CREATE, Statement.RETURN_GENERATED_KEYS)) {
                    stmt.setString(1, sensor.getName());
                    stmt.setString(2, sensor.getType().toString());
                    stmt.setString(3, sensor.getUniqueID());
                    stmt.setTimestamp(4, new Timestamp(System.currentTimeMillis()));
                    stmt.executeUpdate();
                    ResultSet rs = stmt.getGeneratedKeys();
                    if (rs.next()) {
                        sensor.setDbId(rs.getLong(1));
                    }
                }
            } else if (!sensor.getName().equals(name)) {
                // update name
                try (PreparedStatement stmt = conn.prepareStatement(Sensor.STMT_UPD_NAME)) {
                    stmt.setString(1, sensor.getName());
                    stmt.setLong(2, sensor.getDbId());
                    stmt.executeUpdate();
                }
            }

        } catch (SQLException ex) {
            throw new UpdateException("Error updating Sensor DB values", ex);

        }
    }

    private Long createSensorDataEntry(Sensor<?> sensor) {
        if (sensor == null || sensor.getDbId() == null) {
            return null;
        }
        try (Connection conn = dbMan.getConnection()) {
            try (PreparedStatement stmt = conn.prepareStatement(STMT_CREATE_ENTRY, Statement.RETURN_GENERATED_KEYS)) {
                stmt.setLong(1, sensor.getDbId());
                stmt.setTimestamp(2, new Timestamp(sensor.getLastUpdate().getTime()));
                stmt.setBoolean(3, sensor.getOn());
                stmt.setBoolean(4, sensor.getReachable());
                stmt.setInt(5, sensor.getBattery());
                stmt.executeUpdate();
                ResultSet rs = stmt.getGeneratedKeys();
                if (rs.next()) {
                    return rs.getLong(1);
                }
            }
        } catch (SQLException ex) {
            LOG.error("Error Creating data entry", ex);
        }
        return null;
    }

    public ObservableList<Sensor<?>> getSensors() {
        return sensors;
    }

    public Sensor<?> getSensor(int id) {
        return sensorMap.get(id);
    }

}
src/main/java/lu/dainesch/huesense/view/quickview/QuickView.java
public class QuickView extends FXMLView {
    
}
src/main/java/lu/dainesch/huesense/view/mainscreen/sensors/PingSensorView.java
public class PingSensorView extends FXMLView {
    
}
src/main/java/lu/dainesch/huesense/hue/data/Sensor.java
public abstract class Sensor<A> implements Serializable, Comparable<Sensor<?>> {

    public static final String STMT_SEL_UID = "Select S_ID, SENSOR_NAME, SENSOR_TYPE, CREATED from SENSOR where UNIQUE_ID = ?";
    public static final String STMT_UPD_NAME = "Update SENSOR set SENSOR_NAME = ? WHERE S_ID = ?";
    public static final String STMT_CREATE = "Insert into SENSOR (SENSOR_NAME, SENSOR_TYPE, UNIQUE_ID, CREATED) values (?, ?, ?, ?)";

    protected final HueSenseConfig config;
    protected final DBManager dbMan;
    protected final int id;
    protected final String uniqueID;
    protected final SensorType type;

    protected Long dbId;
    protected String name;
    protected Date lastUpdate;
    protected A currentValue;
    protected Boolean on;
    protected Boolean reachable;
    protected int battery = 100;

    protected final IntegerProperty updateCount = new SimpleIntegerProperty(0);
    protected final BooleanProperty quickView = new SimpleBooleanProperty(false);

    public Sensor(int id, String uniqueID, SensorType type, HueSenseConfig config, DBManager dbMan) {
        this.id = id;
        this.type = type;
        this.config = config;
        this.uniqueID = uniqueID;
        this.dbMan = dbMan;

        quickView.set(config.getBoolean(Constants.QV_SENSOR + id));
        quickView.addListener((observable, oldValue, newValue) -> {
            config.putBoolean(Constants.QV_SENSOR + id, newValue);
        });

    }

    public abstract void initSensor();

    public abstract void updateSensor(JsonObject obj) throws UpdateException;

    public abstract void updateView(SensorValue<A> val);

    public abstract String getValueAsString(A value);

    public abstract void saveCurrentValueInDB(Long dataId) throws UpdateException;

    public abstract Set<SensorValue<A>> getValuesInRange(Date start, Date end);

    public abstract String[] getCSVHeader();

    public abstract String[] getCSVEntry(SensorValue<A> val, SimpleDateFormat sdf);

    public void exportCSV(Date start, Date end, SimpleDateFormat sdf, Path file) throws IOException {

        Set<SensorValue<A>> vals = getValuesInRange(start, end);

        try (PrintWriter p = new PrintWriter(new OutputStreamWriter(
                Files.newOutputStream(file, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE), StandardCharsets.UTF_8))) {
            printCSV(p, getCSVHeader());

            vals.forEach((val) -> {
                printCSV(p, getCSVEntry(val, sdf));
            });
        }
    }

    protected void printCSV(PrintWriter p, String... values) {
        for (int i = 0; i < values.length; i++) {
            p.write(values[i]);
            if (i != values.length - 1) {
                p.write(Constants.CSV_SEP);
            }
        }
        p.println();
    }

    public String getCurrentValueAsString() {
        A val = getCurrentValue();
        if (val != null) {
            return getValueAsString(val);
        }
        return "";
    }

    public synchronized void updateValue(SensorValue<A> val) {
        lastUpdate = val.getTime();
        if (currentValue == null || !currentValue.equals(val)) {
            Platform.runLater(() -> updateView(val));
        }
        currentValue = val.getValue();
        Platform.runLater(() -> updateCount.set(updateCount.get() + 1));

    }

    public int getId() {
        return id;
    }

    public String getUniqueID() {
        return uniqueID;
    }

    public SensorType getType() {
        return type;
    }

    public synchronized Long getDbId() {
        return dbId;
    }

    public synchronized void setDbId(Long dbId) {
        this.dbId = dbId;
    }

    public synchronized String getName() {
        return name;
    }

    public synchronized void setName(String name) {
        this.name = name;
    }

    public synchronized Date getLastUpdate() {
        return lastUpdate;
    }

    public synchronized void setLastUpdate(Date lastUpdate) {
        this.lastUpdate = lastUpdate;
    }

    public synchronized A getCurrentValue() {
        return currentValue;
    }

    public synchronized void setCurrentValue(A currentValue) {
        this.currentValue = currentValue;
    }

    public synchronized Boolean getOn() {
        return on;
    }

    public synchronized void setOn(Boolean on) {
        this.on = on;
    }

    public synchronized Boolean getReachable() {
        return reachable;
    }

    public synchronized void setReachable(Boolean reachable) {
        this.reachable = reachable;
    }

    public synchronized int getBattery() {
        return battery;
    }

    public synchronized void setBattery(int battery) {
        this.battery = battery;
    }

    public IntegerProperty getUpdateCount() {
        return updateCount;
    }

    public BooleanProperty getQuickView() {
        return quickView;
    }

    @Override
    public int hashCode() {
        int hash = 7;
        hash = 53 * hash + this.id;
        return hash;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        final Sensor<?> other = (Sensor<?>) obj;
        if (this.id != other.id) {
            return false;
        }
        return true;
    }

    @Override
    public int compareTo(Sensor<?> o) {
        return id - o.id;
    }

    @Override
    public String toString() {
        return name;
    }

    public static class SensorValue<A> implements Comparable<SensorValue<A>> {

        private final Date time;
        private final A value;

        public SensorValue(Date time, A value) {
            this.time = time;
            this.value = value;
        }

        public Date getTime() {
            return time;
        }

        public A getValue() {
            return value;
        }

        @Override
        public int compareTo(SensorValue<A> o) {
            return time.compareTo(o.time);
        }

        @Override
        public int hashCode() {
            int hash = 5;
            hash = 73 * hash + Objects.hashCode(this.time);
            hash = 73 * hash + Objects.hashCode(this.value);
            return hash;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            final SensorValue<?> other = (SensorValue<?>) obj;
            if (!Objects.equals(this.time, other.time)) {
                return false;
            }
            if (!Objects.equals(this.value, other.value)) {
                return false;
            }
            return true;
        }

    }

}
src/main/java/lu/dainesch/huesense/view/mainscreen/list/SensorListCell.java
public class SensorListCell extends ListCell<Sensor<?>> {

    private final static Map<Integer, Node> ITEMVIEWS = new HashMap<>();

    @Override
    protected void updateItem(Sensor<?> item, boolean empty) {
        super.updateItem(item, empty);

        if (empty) {
            setGraphic(null);
        } else {
            Node view = ITEMVIEWS.get(item.getId());
            if (view == null) {
                SensorItemView iv = new SensorItemView();
                ((SensorItemPresenter) iv.getPresenter()).setSensor(item);
                view = iv.getView();
                ITEMVIEWS.put(item.getId(), view);
            }
            setGraphic(view);
        }
    }
}


Question: import lu.dainesch.huesense.view.mainscreen.list.SensorListCell;
import java.net.URL;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.ResourceBundle;
import javafx.animation.Animation;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Platform;
import javafx.collections.ListChangeListener;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ChoiceBox;
import javafx.scene.control.ColorPicker;
import javafx.scene.control.Label;
import javafx.scene.control.ListView;
import javafx.scene.control.ToggleButton;
import javafx.scene.control.Tooltip;
import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import javax.inject.Inject;
import lu.dainesch.huesense.Constants;
import lu.dainesch.huesense.HueSenseConfig;
import lu.dainesch.huesense.hue.DataStore;
import lu.dainesch.huesense.hue.HueComm;
import lu.dainesch.huesense.hue.data.LightSensor;
import lu.dainesch.huesense.hue.data.PingSensor;
import lu.dainesch.huesense.hue.data.PresenceSensor;
import lu.dainesch.huesense.hue.data.Sensor;
import lu.dainesch.huesense.hue.data.TempSensor;
import lu.dainesch.huesense.view.UIUtils;
import lu.dainesch.huesense.view.about.AboutView;
import lu.dainesch.huesense.view.alarm.AlarmConfigView;
import lu.dainesch.huesense.view.export.ExportView;
import lu.dainesch.huesense.view.mainscreen.sensors.LightSensorPresenter;
import lu.dainesch.huesense.view.mainscreen.sensors.LightSensorView;
import lu.dainesch.huesense.view.mainscreen.sensors.PingSensorPresenter;
import lu.dainesch.huesense.view.mainscreen.sensors.PingSensorView;
import lu.dainesch.huesense.view.mainscreen.sensors.PresenceSensorPresenter;
import lu.dainesch.huesense.view.mainscreen.sensors.PresenceSensorView;
import lu.dainesch.huesense.view.mainscreen.sensors.TempSensorPresenter;
import lu.dainesch.huesense.view.mainscreen.sensors.TempSensorView;
import lu.dainesch.huesense.view.quickview.QuickPresenter;
import lu.dainesch.huesense.view.quickview.QuickView;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
    private ImageView logoImg;
    @FXML
    private ToggleButton alarmBut;
    @FXML
    private Button alarmSettBut;
    @FXML
    private Button aboutBut;
    @FXML
    private Button exportBut;

    @Inject
    private HueComm hue;
    @Inject
    private DataStore store;
    @Inject
    private HueSenseConfig config;

    private Stage stage;
    private TrayView trayView;
    private QuickPresenter quickPres;
    private SimpleDateFormat sdf;
    private Timeline pollTimeLine;
    private Map<Integer, Node> sensorViews;

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        trayView = new TrayView();
        sensorViews = new HashMap<>();
        updLabel.setText("Never");
        logoImg.setImage(UIUtils.getImage("logo.png"));
        sdf = new SimpleDateFormat(Constants.TIMEFORMAT);

        sensorList.setCellFactory((param) -> {
            return new SensorListCell();
        });
        sensorList.setItems(store.getSensors());
        sensorList.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> {
            if (newValue != null) {
                showSensor(newValue);
            }
        });
        store.getSensors().addListener((ListChangeListener.Change<? extends Sensor<?>> c) -> {
            if (sensorList.getSelectionModel().getSelectedItem() == null) {
                sensorList.getSelectionModel().selectFirst();
            }
        });

        //TODO finish impl
        alarmSettBut.setVisible(false);
        alarmBut.setVisible(false);

        hideBut.setGraphic(new ImageView(UIUtils.getImage("hide.png")));
        quickBut.setGraphic(new ImageView(UIUtils.getImage("quickview.png")));
        alarmBut.setGraphic(new ImageView(UIUtils.getImage("alarm.png")));
        alarmSettBut.setGraphic(new ImageView(UIUtils.getImage("alarm_sett.png")));
        aboutBut.setGraphic(new ImageView(UIUtils.getImage("about.png")));
        exportBut.setGraphic(new ImageView(UIUtils.getImage("export.png")));
        hideBut.setSelected(config.getBoolean(Constants.HIDE_ON_CLOSE));
        quickBut.setSelected(config.getBoolean(Constants.SHOW_QV));
        onQuickBut(null);

        hideBut.setTooltip(new Tooltip("Minimizes the application to the tray instead of closing it"));
        quickBut.setTooltip(new Tooltip("Display a minimal view with current sensor data"));

        colorPicker.setTooltip(new Tooltip("Color of the QuickView text"));

        updCB.setItems(PollInterval.INTERVALS);
        updCB.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> {
            setPollInterval(newValue);
        });
        updCB.getSelectionModel().select(1);
        onRefresh(null);
        updCB.setTooltip(new Tooltip("Update interval of the sensor data"));

        stage = new Stage();
        stage.setTitle("HueSense");
        stage.setOnCloseRequest((ev) -> {
            if (hideBut.isSelected() && trayView.isSupported()) {
                trayView.showTrayIcon();
                ev.consume();
                stage.hide();
            } else {
                Platform.exit();
                System.exit(0);
            }
        });
        trayView.setOpenListener((observable, oldValue, newValue) -> {
            trayView.hideTrayIcon();
            stage.show();
        });
        trayView.setExitListener((observable, oldValue, newValue) -> {
            trayView.hideTrayIcon();
            Platform.exit();
            System.exit(0);
        });

        Scene scene = new Scene(mainPane);
        scene.setFill(Color.WHITE);
        final String uri = Thread.currentThread().getContextClassLoader().getResource("global.css").toExternalForm();
        scene.getStylesheets().add(uri);
        stage.setScene(scene);
        stage.centerOnScreen();
        UIUtils.setIcon(stage);
        stage.show();

    }

    private void showSensor(Sensor<?> sensor) {
        Node o = sensorViews.get(sensor.getId());
        if (o == null) {
            switch (sensor.getType()) {
                case LIGHT:
                    LightSensorView lview = new LightSensorView();
                    o = lview.getView();
                    ((LightSensorPresenter) lview.getPresenter()).setSensor((LightSensor) sensor);
                    sensorViews.put(sensor.getId(), o);
                    break;
                case PRESENCE:
                    PresenceSensorView pview = new PresenceSensorView();
                    o = pview.getView();
Answer:                     ((PresenceSensorPresenter) pview.getPresenter()).setSensor((PresenceSensor) sensor);