com.imaginary.home.device.hue.HueBulb.java Source code

Java tutorial

Introduction

Here is the source code for com.imaginary.home.device.hue.HueBulb.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.device.hue;

import com.imaginary.home.controller.CommunicationException;
import com.imaginary.home.controller.ControllerException;
import com.imaginary.home.controller.HomeController;
import com.imaginary.home.lighting.Color;
import com.imaginary.home.lighting.ColorMode;
import com.imaginary.home.lighting.Light;
import org.apache.log4j.Logger;
import org.dasein.util.uom.time.TimePeriod;
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.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;

public class HueBulb implements Light {
    static public final Logger logger = Hue.getLogger(HueBulb.class);

    private String bulbId;
    private Hue hue;
    private String name;

    public HueBulb(@Nonnull Hue hue, @Nonnull String id, @Nonnull String name) {
        bulbId = id;
        this.hue = hue;
        this.name = name;
    }

    @Override
    public @Nonnull Future<Boolean> changeColor(final @Nonnull Color newColor,
            final @Nullable TimePeriod<?> transitionTime) {
        return HomeController.executorService.submit(new Callable<Boolean>() {
            @Override
            public Boolean call() throws HueException {
                long millis = (transitionTime == null ? 0L
                        : transitionTime.convertTo(TimePeriod.MILLISECOND).longValue());

                return changeColor(newColor, millis);
            }
        });
    }

    @Override
    public @Nonnull Future<Boolean> changeColorCIEXYZ(@Nonnegative float x, @Nonnegative float y,
            @Nullable TimePeriod<?> transitionTime) throws CommunicationException {
        return changeColor(new Color(ColorMode.CIEXYZ, x, y, 1.0f - (x + y)), transitionTime);
    }

    @Override
    public @Nonnull Future<Boolean> changeColorHSV(@Nonnegative float hue, @Nonnegative float saturation,
            @Nullable TimePeriod<?> transitionTime) throws CommunicationException {
        return changeColor(new Color(ColorMode.HSV, hue, saturation), transitionTime);
    }

    @Override
    public @Nonnull Future<Boolean> changeColorRGB(@Nonnegative float red, @Nonnegative float green,
            @Nonnegative float blue, @Nullable TimePeriod<?> transitionTime) throws CommunicationException {
        return changeColor(new Color(ColorMode.RGB, red, green, blue), transitionTime);
    }

    private boolean changeColor(@Nonnull Color newColor, @Nonnegative long millis) throws HueException {
        Map<String, Object> state = new HashMap<String, Object>();
        String resource = "lights/" + bulbId + "/state";
        float[] components = newColor.getComponents();

        if (millis >= 100) {
            state.put("transitiontime", millis / 100);
        }
        state.put("on", true);
        if (newColor.getColorMode().equals(ColorMode.HSV)) {
            float h = components[0];
            int s = (int) components[1];

            if (h < 0) {
                h = 0;
            } else if (h > 360) {
                h = 360;
            }
            if (s < 0) {
                s = 0;
            } else if (s > 100) {
                s = 100;
            }

            state.put("hue", (int) (182.04 * h));
            state.put("sat", (s * 254) / 100);
        } else if (newColor.getColorMode().equals(ColorMode.CT)) {
            state.put("ct", (int) components[0]);
            state.put("bri", (int) (components[1] * 254 / 100));
        } else {
            if (!newColor.getColorMode().equals(ColorMode.CIEXYZ)) {
                newColor = newColor.convertToCIEXYZ();
                components = newColor.getComponents();
            }
            // More information: 
            // https://github.com/PhilipsHue/PhilipsHueSDKiOS/blob/master/ApplicationDesignNotes/RGB%20to%20xy%20Color%20conversion.md
            float x = components[0] / (components[0] + components[1] + components[2]);
            float y = components[1] / (components[0] + components[1] + components[2]);

            state.put("xy", new float[] { x, y });
        }
        HueMethod method = new HueMethod(hue);

        method.put(resource, new JSONObject(state));
        return true;
    }

    @Override
    public @Nonnull Future<Boolean> changeWhite(final @Nonnegative int warmthInMireds,
            final @Nonnegative float brightness, final @Nullable TimePeriod<?> transitionTime) {
        return HomeController.executorService.submit(new Callable<Boolean>() {
            @Override
            public Boolean call() throws HueException {
                long millis = (transitionTime == null ? 0L
                        : transitionTime.convertTo(TimePeriod.MILLISECOND).longValue());

                return changeWhite(warmthInMireds, brightness, millis);
            }
        });
    }

    private boolean changeWhite(int warmth, float brightness, @Nonnegative long millis) throws HueException {
        return changeColor(new Color(ColorMode.CT, warmth, brightness), millis);
    }

    @Override
    public @Nonnull Future<Boolean> fadeOff(final @Nonnull TimePeriod<?> transitionTime)
            throws CommunicationException {
        return HomeController.executorService.submit(new Callable<Boolean>() {
            @Override
            public Boolean call() throws CommunicationException {
                return fade(transitionTime, 0);
            }
        });
    }

    @Override
    public @Nonnull Future<Boolean> fadeOn(@Nonnull TimePeriod<?> transitionTime) throws CommunicationException {
        return fadeOn(transitionTime, getBrightness());
    }

    @Override
    public @Nonnull Future<Boolean> fadeOn(final @Nonnull TimePeriod<?> transitionTime,
            final @Nonnegative float targetBrightness) {
        return HomeController.executorService.submit(new Callable<Boolean>() {
            @Override
            public Boolean call() throws CommunicationException {
                return fade(transitionTime, (int) ((targetBrightness * 254) / 100));
            }
        });
    }

    private @Nonnegative boolean fade(@Nonnull TimePeriod<?> transitionTime, @Nonnegative int targetBrightness)
            throws CommunicationException {
        logger.debug("Fading to " + targetBrightness + " over " + transitionTime + " for " + bulbId);
        if (targetBrightness < 0) {
            targetBrightness = 0;
        }
        int currentBrightness = (int) ((getBrightness() * 254) / 100);
        boolean on = isOn();

        if ((on && targetBrightness == currentBrightness) || (!on && targetBrightness == 0)) {
            if (logger.isDebugEnabled()) {
                logger.debug("Nothing to do for " + bulbId);
            }
            return false;
        }
        if (!on) {
            logger.debug("Bulb is currently off");
            currentBrightness = 0;
        }
        if (logger.isDebugEnabled()) {
            logger.debug(bulbId + ".currentBrightness=" + currentBrightness);
        }
        int distance;

        if (targetBrightness == 0) {
            distance = currentBrightness;
        } else if (targetBrightness > currentBrightness) {
            distance = targetBrightness - currentBrightness;
        } else {
            distance = currentBrightness - targetBrightness;
        }
        if (logger.isDebugEnabled()) {
            logger.debug(bulbId + ".distance=" + distance);
        }
        int conversionTime = transitionTime.convertTo(TimePeriod.MILLISECOND).intValue() / 100;
        Map<String, Object> state = new HashMap<String, Object>();
        long startTimestamp = System.currentTimeMillis();
        String resource = "lights/" + bulbId + "/state";
        HueMethod method = new HueMethod(hue);

        if (conversionTime < 1) {
            conversionTime = 1;
        }
        if (!on) {
            state.put("on", true);
            state.put("bri", 0);
            if (logger.isDebugEnabled()) {
                logger.debug("Turning " + bulbId + " on...");
            }
            try {
                method.put(resource, new JSONObject(state));
            } catch (Throwable t) {
                t.printStackTrace();
            }
        }

        int interval = 100;
        int step = 1;

        if (conversionTime < distance) {
            step = distance / conversionTime;
        } else {
            interval = (conversionTime / distance) * 100;
        }
        if (logger.isDebugEnabled()) {
            logger.debug(bulbId + " -> interval + =" + interval + ", step=" + step);
        }
        int stepCount = distance / step;
        if (logger.isDebugEnabled()) {
            logger.debug(bulbId + ".stepCount=" + stepCount);
        }
        if (targetBrightness < currentBrightness) {
            step = -step;
        }
        state.clear();
        for (int i = 0; i < stepCount; i++) {
            try {
                Thread.sleep(interval);
            } catch (InterruptedException ignore) {
            }
            // adjust for slow network
            int actual = (int) (System.currentTimeMillis() - startTimestamp) / interval;
            int bri = currentBrightness + (actual * step);

            if (bri <= 254 && bri >= 0) {
                state.put("bri", bri);
                if (logger.isTraceEnabled()) {
                    logger.debug(bulbId + " -> setting brightness to " + bri);
                }
                try {
                    method.put(resource, new JSONObject(state));
                } catch (HueException ignore) {
                }
            }
            if (bri == targetBrightness) {
                break;
            }
        }
        if (getBrightness() != targetBrightness) {
            if (logger.isDebugEnabled()) {
                logger.debug(bulbId + " -> cleaning up");
            }
            state.clear();
            state.put("bri", targetBrightness);
            try {
                method.put(resource, new JSONObject(state));
            } catch (HueException ignore) {
            }
        }
        if (targetBrightness == 0) {
            if (logger.isDebugEnabled()) {
                logger.debug(bulbId + " -> turning off");
            }
            flip(false);
        }
        int brightness = (int) ((getBrightness() * 254) / 100);

        if (logger.isDebugEnabled()) {
            logger.debug(bulbId + ".brightness=" + brightness);
        }
        return true;
    }

    @Override
    public @Nonnull Future<Boolean> flipOff() {
        return HomeController.executorService.submit(new Callable<Boolean>() {
            @Override
            public Boolean call() throws CommunicationException {
                return flip(false);
            }
        });
    }

    @Override
    public @Nonnull Future<Boolean> flipOn() throws CommunicationException {
        return HomeController.executorService.submit(new Callable<Boolean>() {
            @Override
            public Boolean call() throws CommunicationException {
                return flip(true);
            }
        });
    }

    private boolean flip(boolean on) throws CommunicationException {
        if (isOn() == on) {
            return false;
        }
        Map<String, Object> state = new HashMap<String, Object>();

        state.put("on", on);
        String resource = "lights/" + bulbId + "/state";
        HueMethod method = new HueMethod(hue);

        method.put(resource, new JSONObject(state));
        return true;
    }

    @Override
    public @Nonnegative float getBrightness() throws CommunicationException {
        String resource = "lights/" + bulbId;
        HueMethod method = new HueMethod(hue);

        JSONObject json = method.get(resource);

        if (json == null || !json.has("state")) {
            return 0;
        }
        try {
            JSONObject state = json.getJSONObject("state");

            if (!state.has("bri")) {
                return 0f;
            }
            return ((float) state.getInt("bri") * 100f) / 254;
        } catch (JSONException e) {
            throw new HueException(e);
        }
    }

    @Override
    public @Nonnull Color getColor() throws CommunicationException {
        String resource = "lights/" + bulbId;
        HueMethod method = new HueMethod(hue);

        JSONObject json = method.get(resource);

        if (json == null || !json.has("state")) {
            return new Color(ColorMode.CIEXYZ, 0.4448f, 0.4066f);
        }
        try {
            JSONObject state = json.getJSONObject("state");

            if (state.has("colormode")) {
                String mode = state.getString("colormode");

                if (mode.equalsIgnoreCase("xy")) {
                    JSONArray arr = state.getJSONArray("xy");

                    new Color(ColorMode.CIEXYZ, (float) arr.getDouble(0), (float) arr.getDouble(1));
                } else if (mode.equalsIgnoreCase("ct")) {
                    int warmth = (state.has("ct") ? state.getInt("ct") : 154);
                    int brightness = (state.has("bri") ? state.getInt("bri") : 0);

                    return new Color(ColorMode.CT, warmth, brightness / 254);
                } else {
                    float h = (state.has("hue") ? (float) state.getDouble("hue") / 182.04f : 360f);
                    int s = (state.has("sat") ? (state.getInt("sat") * 100) / 255 : 100);

                    return new Color(ColorMode.HSV, h, s);
                }
            }
            return new Color(ColorMode.CIEXYZ, 0.4448f, 0.4066f);
        } catch (JSONException e) {
            throw new HueException(e);
        }
    }

    @Override
    public @Nonnull ColorMode getColorMode() throws CommunicationException {
        String resource = "lights/" + bulbId;
        HueMethod method = new HueMethod(hue);

        JSONObject json = method.get(resource);

        if (json == null || !json.has("state")) {
            return ColorMode.CIEXYZ;
        }
        try {
            JSONObject state = json.getJSONObject("state");

            if (state.has("colormode")) {
                String mode = state.getString("colormode");

                if (mode.equalsIgnoreCase("hue")) {
                    return ColorMode.HSV;
                } else if (mode.equalsIgnoreCase("ct")) {
                    return ColorMode.CT;
                }
                return ColorMode.CIEXYZ;
            }
            return ColorMode.CIEXYZ;
        } catch (JSONException e) {
            throw new HueException(e);
        }
    }

    @Override
    public @Nullable String getModel() throws CommunicationException {
        String resource = "lights/" + bulbId;
        HueMethod method = new HueMethod(hue);

        JSONObject json = method.get(resource);

        if (json == null || !json.has("modelid")) {
            return null;
        }
        try {
            return json.getString("modelid");
        } catch (JSONException e) {
            throw new HueException(e);
        }
    }

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

    @Override
    public @Nonnull String getProviderId() {
        return bulbId;
    }

    @Override
    public boolean isOn() throws CommunicationException {
        String resource = "lights/" + bulbId;
        HueMethod method = new HueMethod(hue);

        JSONObject json = method.get(resource);

        if (json == null || !json.has("state")) {
            return false;
        }
        try {
            JSONObject state = json.getJSONObject("state");

            return (state.has("on") && state.getBoolean("on"));
        } catch (JSONException e) {
            throw new HueException(e);
        }
    }

    @Override
    public Future<Boolean> strobe(final @Nonnull TimePeriod<?> interval, final @Nullable TimePeriod<?> duration,
            final @Nonnull Color... colors) throws CommunicationException {
        return HomeController.executorService.submit(new Callable<Boolean>() {
            @Override
            public Boolean call() throws HueException {
                long end = (duration == null ? 0L
                        : (System.currentTimeMillis() + duration.convertTo(TimePeriod.MILLISECOND).longValue()));
                long millis = interval.convertTo(TimePeriod.MILLISECOND).longValue();
                int currentColor = 0;

                if (millis < 100) {
                    millis = 100;
                }
                while (end < 1 || System.currentTimeMillis() < end) {
                    Color color = colors[currentColor++];

                    if (currentColor >= colors.length) {
                        currentColor = 0;
                    }
                    changeColor(color, null);
                    try {
                        Thread.sleep(millis);
                    } catch (InterruptedException ignore) {
                    }
                }
                return true;
            }
        });
    }

    @Override
    public boolean supportsBrightnessChanges() {
        return true;
    }

    @Override
    public boolean supportsColor() {
        return true;
    }

    @Override
    public boolean supportsColorChanges() {
        return true;
    }

    @Override
    public void toMap(@Nonnull Map<String, Object> map) throws CommunicationException {
        String resource = "lights/" + bulbId;
        HueMethod method = new HueMethod(hue);

        JSONObject json = method.get(resource);

        if (json == null || !json.has("state")) {
            throw new CommunicationException("No state found for " + bulbId);
        }
        try {
            JSONObject state = json.getJSONObject("state");

            map.put("on", state.has("on") && !state.isNull("on") && state.getBoolean("on"));
            if (state.has("modelid") && !state.isNull("modelid")) {
                map.put("model", state.getString("modelid"));
            }
            if (state.has("colormode")) {
                HashMap<String, Object> color = new HashMap<String, Object>();

                String mode = state.getString("colormode");

                if (mode.equalsIgnoreCase("xy")) {
                    JSONArray arr = state.getJSONArray("xy");

                    color.put("colorMode", ColorMode.CIEXYZ);
                    color.put("components", new float[] { (float) arr.getDouble(0), (float) arr.getDouble(1) });
                } else if (mode.equalsIgnoreCase("ct")) {
                    int warmth = (state.has("ct") ? state.getInt("ct") : 154);
                    int brightness = (state.has("bri") ? state.getInt("bri") : 0);

                    color.put("colorMode", ColorMode.CT);
                    color.put("components", new float[] { warmth, brightness / 254 });
                } else {
                    float h = (state.has("hue") ? (float) state.getDouble("hue") / 182.04f : 360f);
                    int s = (state.has("sat") ? (state.getInt("sat") * 100) / 255 : 100);

                    color.put("colorMode", ColorMode.HSV);
                    color.put("components", new float[] { h, s });
                }
                map.put("color", color);
            } else {
                throw new CommunicationException("No color information is present");
            }
        } catch (JSONException e) {
            throw new HueException(e);
        }
        map.put("deviceId", bulbId);
        map.put("supportsColorChanges", supportsColorChanges());
        map.put("supportsBrightnessChanges", supportsBrightnessChanges());
        map.put("name", name);
        map.put("description", name);
        map.put("deviceType", "light");
        map.put("systemId", hue.getId());
        ArrayList<ColorMode> modes = new ArrayList<ColorMode>();

        for (ColorMode m : hue.listNativeColorModes()) {
            modes.add(m);
        }
        map.put("colorModes", modes);
    }

    public String toString() {
        return (name + " [#" + bulbId + "]");
    }
}