/* HueLightProfile Copyright (c) 2015 NTT DOCOMO,INC. Released under the MIT license http://opensource.org/licenses/mit-license.php */ package org.deviceconnect.android.deviceplugin.hue.profile; import android.content.Intent; import android.os.Bundle; import com.philips.lighting.hue.listener.PHLightListener; import com.philips.lighting.hue.sdk.PHHueSDK; import com.philips.lighting.hue.sdk.utilities.PHUtilities; import com.philips.lighting.model.PHBridge; import com.philips.lighting.model.PHBridgeResource; import com.philips.lighting.model.PHBridgeResourcesCache; import com.philips.lighting.model.PHHueError; import com.philips.lighting.model.PHLight; import com.philips.lighting.model.PHLight.PHLightColorMode; import com.philips.lighting.model.PHLightState; import org.deviceconnect.android.deviceplugin.hue.R; import org.deviceconnect.android.message.MessageUtils; import org.deviceconnect.android.profile.LightProfile; import org.deviceconnect.android.profile.api.DeleteApi; import org.deviceconnect.android.profile.api.GetApi; import org.deviceconnect.android.profile.api.PostApi; import org.deviceconnect.android.profile.api.PutApi; import org.deviceconnect.android.service.DConnectService; import org.deviceconnect.message.DConnectMessage; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; /** * 親クラスで振り分けられたメソッドに対して、Hueのlight attribute処理を呼び出す. * * @author NTT DOCOMO, INC. */ public class HueLightProfile extends LightProfile { /** * hue minimum brightness value. */ private static final int HUE_BRIGHTNESS_MIN_VALUE = 1; /** * hue maximum brightness value. */ private static final int HUE_BRIGHTNESS_MAX_VALUE = 255; /** * hue SDK maximum brightness value. */ private static final int HUE_BRIGHTNESS_TUNED_MAX_VALUE = 254; /** * ライトフラッシング管理マップ. */ private final Map<String, FlashingExecutor> mFlashingMap = new HashMap<String, FlashingExecutor>(); public HueLightProfile() { addApi(new GetApi() { @Override public boolean onRequest(final Intent request, final Intent response) { String serviceId = getServiceID(request); PHBridge bridge = findBridge(serviceId); if (bridge == null) { MessageUtils.setNotFoundServiceError(response, "Not found bridge: " + serviceId); return true; } List<Bundle> lightList = new ArrayList<Bundle>(); for (PHLight phLight : bridge.getResourceCache().getAllLights()) { PHLightState phState = phLight.getLastKnownLightState(); Bundle light = new Bundle(); setLightId(light, phLight.getIdentifier()); setName(light, phLight.getName()); setOn(light, phState != null ? phState.isOn() : false); setConfig(light, ""); lightList.add(light); } setLights(response, lightList); sendResultOK(response); return true; } }); addApi(new PostApi() { @Override public boolean onRequest(final Intent request, final Intent response) { String serviceId = getServiceID(request); String lightId = getLightId(request); Integer color = getColor(request); Double brightness = getBrightness(request); long[] flashing = getFlashing(request); final PHBridge bridge = findBridge(serviceId); if (bridge == null) { MessageUtils.setNotFoundServiceError(response, "Not found bridge: " + serviceId); return true; } final PHLight light = findLight(bridge, lightId); if (light == null) { MessageUtils.setInvalidRequestParameterError(response, "Not found light: " + lightId + "@" + serviceId); return true; } final PHLightState lightState = makeLightState(color, brightness, flashing); if (flashing != null) { flashing(lightId, lightState, bridge, light, flashing); sendResultOK(response);//do not check result of flashing return true; } else { bridge.updateLightState(light, lightState, new PHLightAdapter() { @Override public void onStateUpdate(final Map<String, String> successAttribute, final List<PHHueError> errorAttribute) { sendResultOK(response); } @Override public void onError(final int code, final String message) { if (code == PHHueError.AUTHENTICATION_FAILED) { disconnectHueBridge(bridge); } MessageUtils.setUnknownError(response, code + ": " + message); sendResultERR(response); } }); return false; } } }); addApi(new DeleteApi() { @Override public boolean onRequest(final Intent request, final Intent response) { String serviceId = getServiceID(request); String lightId = getLightId(request); final PHBridge bridge = findBridge(serviceId); if (bridge == null) { MessageUtils.setNotFoundServiceError(response, "Not found bridge: " + serviceId); return true; } PHLight light = findLight(bridge, lightId); if (light == null) { MessageUtils.setInvalidRequestParameterError(response, "Not found light: " + lightId + "@" + serviceId); return true; } PHLightState lightState = new PHLightState(); lightState.setOn(false); bridge.updateLightState(light, lightState, new PHLightAdapter() { @Override public void onStateUpdate(final Map<String, String> successAttribute, final List<PHHueError> errorAttribute) { sendResultOK(response); } @Override public void onError(final int code, final String message) { if (code == PHHueError.AUTHENTICATION_FAILED) { disconnectHueBridge(bridge); } String errMsg = getContext().getString( R.string.error_message_failed_to_update_light, code, message); MessageUtils.setUnknownError(response, errMsg); sendResultERR(response); } }); return false; } }); addApi(new PutApi() { @Override public boolean onRequest(final Intent request, final Intent response) { String serviceId = getServiceID(request); String lightId = getLightId(request); Integer color = getColor(request); Double brightness = getBrightness(request); long[] flashing = getFlashing(request); String name = getName(request); final PHBridge bridge = findBridge(serviceId); if (bridge == null) { MessageUtils.setNotFoundServiceError(response, "Not found bridge: " + serviceId); return true; } final PHLight light = findLight(bridge, lightId); if (light == null) { MessageUtils.setInvalidRequestParameterError(response, "Not found light: " + lightId + "@" + serviceId); return true; } if (name == null || name.length() == 0) { MessageUtils.setInvalidRequestParameterError(response, "name is invalid."); return true; } //wait for change name and status final CountDownLatch countDownLatch = new CountDownLatch(2); sendResponseAfterAwait(response, countDownLatch); PHLight newLight = new PHLight(light); newLight.setName(name); bridge.updateLight(newLight, new PHLightAdapter() { private boolean mErrorFlag = false; @Override public void onSuccess() { super.onSuccess(); countDown(); } @Override public void onError(final int code, final String message) { super.onError(code, message); String errMsg = getContext().getString( R.string.error_message_failed_to_update_light, code, message); MessageUtils.setUnknownError(response, errMsg); mErrorFlag = true; countDown(); } private void countDown() { if (!mErrorFlag) { setResult(response, DConnectMessage.RESULT_OK); } countDownLatch.countDown(); } }); final PHLightState lightState = makeLightState(color, brightness, flashing); if (flashing != null) { flashing(lightId, lightState, bridge, light, flashing); countDownLatch.countDown();//do not check result of flashing } else { bridge.updateLightState(light, lightState, new PHLightAdapter() { private boolean mErrorFlag = false; @Override public void onStateUpdate(final Map<String, String> successAttribute, final List<PHHueError> errorAttribute) { countDown(); } @Override public void onError(final int code, final String message) { if (code == PHHueError.AUTHENTICATION_FAILED) { disconnectHueBridge(bridge); } String errMsg = getContext().getString( R.string.error_message_failed_to_update_light, code, message); MessageUtils.setUnknownError(response, errMsg); mErrorFlag = true; countDown(); } private void countDown() { if (!mErrorFlag) { setResult(response, DConnectMessage.RESULT_OK); } countDownLatch.countDown(); } }); } return false; } }); } private PHLightState makeLightState(Integer color, Double brightness, long[] flashing) { int[] colors = convertColor(color); // Brightness magnification conversion calcColorParam(colors, brightness); // Calculation of brightness. int calcBrightness = calcBrightnessParam(colors); PHLightState lightState = new PHLightState(); lightState.setOn(true); lightState.setColorMode(PHLightColorMode.COLORMODE_XY); Color hueColor = new Color(color); lightState.setX(hueColor.mX); lightState.setY(hueColor.mY); lightState.setBrightness(calcBrightness); if (flashing != null) { lightState.setTransitionTime(1); } return lightState; } private void flashing(String lightId, final PHLightState lightState, final PHBridge bridge, final PHLight light, long[] flashing) { FlashingExecutor exe = mFlashingMap.get(lightId); if (exe == null) { exe = new FlashingExecutor(); mFlashingMap.put(lightId, exe); } exe.setLightControllable(new FlashingExecutor.LightControllable() { @Override public void changeLight(final boolean isOn, final FlashingExecutor.CompleteListener listener) { lightState.setOn(isOn); bridge.updateLightState(light, lightState, new PHLightAdapter() { @Override public void onStateUpdate(final Map<String, String> successAttribute, final List<PHHueError> errorAttribute) { listener.onComplete(); } @Override public void onError(final int code, final String message) { listener.onComplete(); } }); } }); exe.start(flashing); } private void sendResponseAfterAwait(final Intent response, final CountDownLatch latch) { new Thread(new Runnable() { @Override public void run() { try { if (!latch.await(30, TimeUnit.SECONDS)) { MessageUtils.setTimeoutError(response); } } catch (InterruptedException e) { MessageUtils.setTimeoutError(response); } sendResponse(response); } }).start(); } /** * Convert Integer to int[]. * * @param color color * @return int[] */ private int[] convertColor(final Integer color) { int[] colors = new int[3]; if (color != null) { colors[0] = android.graphics.Color.red(color); colors[1] = android.graphics.Color.green(color); colors[2] = android.graphics.Color.blue(color); } else { colors[0] = 0xFF; colors[1] = 0xFF; colors[2] = 0xFF; } return colors; } /** * Hueのブリッジを検索する. * * @param serviceId Service ID * @return Hueのブリッジを管理するオブジェクト */ private PHBridge findBridge(final String serviceId) { List<PHBridge> bridges = PHHueSDK.getInstance().getAllBridges(); for (PHBridge bridge : bridges) { PHBridgeResourcesCache cache = bridge.getResourceCache(); String ipAddress = cache.getBridgeConfiguration().getIpAddress(); if (serviceId.equals(ipAddress)) { return bridge; } } return null; } /** * Hueに接続されているライトを検索する. * <p> * ライトが見つからない場合には<code>null</code>を返却する。 * </p> * * @param bridge Hueのブリッジ * @param lightId ライトID * @return Hueのブリッジに接続されたライト */ private PHLight findLight(final PHBridge bridge, final String lightId) { Map<String, PHLight> lights = bridge.getResourceCache().getLights(); if (lights.size() == 0) { return null; } if (lightId == null || lightId.length() == 0) { return lights.entrySet().iterator().next().getValue(); } else { return bridge.getResourceCache().getLights().get(lightId); } } /** * 認証が失敗した場合にブリッジを切断します. * @param bridge 切断するブリッジ */ private void disconnectHueBridge(final PHBridge bridge) { if (bridge == null) { return; } PHHueSDK hueSDK = PHHueSDK.getInstance(); hueSDK.disconnect(bridge); DConnectService service = getService(); service.setOnline(false); } /** * 成功レスポンス送信. * * @param response response */ private void sendResultOK(final Intent response) { setResult(response, DConnectMessage.RESULT_OK); sendResponse(response); } /** * エラーレスポンスを送信する. * * @param response エラーレスポンス */ private void sendResultERR(final Intent response) { setResult(response, DConnectMessage.RESULT_ERROR); sendResponse(response); } /** * Calculate color parameter. * * @param color Color parameters. * @param brightness Brightness parameter. */ private void calcColorParam(final int[] color, final Double brightness) { if (brightness != null) { color[0] = (int) Math.round(color[0] * brightness); color[1] = (int) Math.round(color[1] * brightness); color[2] = (int) Math.round(color[2] * brightness); } } /** * Calculate brightness parameter. * * @param color Color parameters. * @return brightness Brightness parameter. */ private int calcBrightnessParam(final int[] color) { int brightness = Math.max(color[0], color[1]); brightness = Math.max(brightness, color[2]); if (brightness < HUE_BRIGHTNESS_MIN_VALUE) { brightness = HUE_BRIGHTNESS_MIN_VALUE; } else if (brightness >= HUE_BRIGHTNESS_MAX_VALUE) { brightness = HUE_BRIGHTNESS_TUNED_MAX_VALUE; } return brightness; } /** * Hueの色指定. * * @author NTT DOCOMO, INC. */ private static class Color { /** * モデル. */ private static final String MODEL = "LCT001"; /** * R. */ final int mR; /** * G. */ final int mG; /** * B. */ final int mB; /** * 色相のX座標. */ final float mX; /** * 色相のY座標. */ final float mY; /** * コンストラクタ. * * @param rgb RGB */ Color(final int rgb) { mR = android.graphics.Color.red(rgb); mG = android.graphics.Color.green(rgb); mB = android.graphics.Color.blue(rgb); float[] xy = PHUtilities.calculateXYFromRGB(mR, mG, mB, MODEL); mX = xy[0]; mY = xy[1]; } /** * コンストラクタ. * * @param rgb RGB */ Color(final Integer rgb) { this(rgb != null ? rgb : 0xFFFFFF); } } /** * ライトのアダプター. * * @author NTT DOCOMO, INC. */ private static class PHLightAdapter implements PHLightListener { @Override public void onError(final int code, final String message) { } @Override public void onStateUpdate(final Map<String, String> successAttribute, final List<PHHueError> errorAttribute) { } @Override public void onSuccess() { } @Override public void onReceivingLightDetails(final PHLight light) { } @Override public void onReceivingLights(final List<PHBridgeResource> lights) { } @Override public void onSearchComplete() { } } }