Java tutorial
/** * Copyright (c) 2010-2015, openHAB.org and others. * <p> * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html */ package org.openhab.binding.yeelight.internal; import com.google.gson.Gson; import org.apache.commons.lang.StringUtils; import org.openhab.binding.yeelight.YeelightBindingProvider; import org.openhab.binding.yeelight.model.YeelightGetPropsResponse; import org.openhab.core.binding.AbstractActiveBinding; import org.openhab.core.items.ItemNotFoundException; import org.openhab.core.items.ItemRegistry; import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.HSBType; import org.openhab.core.library.types.OnOffType; import org.openhab.core.library.types.PercentType; import org.openhab.core.types.Command; import org.openhab.core.types.State; import org.osgi.framework.BundleContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.awt.*; import java.io.BufferedReader; import java.io.DataOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.net.*; import java.util.ArrayList; import java.util.Hashtable; import java.util.Map; /** * Implement this class if you are going create an actively polling service * like querying a Website/Device. * * @author Ondrej Pecta * @since 1.9.0 */ public class YeelightBinding extends AbstractActiveBinding<YeelightBindingProvider> { private static final Logger logger = LoggerFactory.getLogger(YeelightBinding.class); private static final String MCAST_ADDR = "239.255.255.250"; private static final int MCAST_PORT = 1982; private final int BUFFER_LENGTH = 1024; private long msgid = 0; //Constants private final String RESULT = "result"; private final String TOGGLE = "toggle"; private final String NIGHTLIGHT = "nightlight"; private final String SMOOTH = "smooth"; private final String GET_PROP = "get_prop"; private final String SET_BRIGHT = "set_bright"; private final String SET_SCENE = "set_scene"; private final String SET_POWER = "set_power"; private final String SET_CT = "set_ct"; private final String SET_HSB = "set_hsb"; private final String SET_RGB = "set_rgb"; //thread private Thread thread; //Socket private MulticastSocket socket = null; //Gson private Gson gson = new Gson(); //devices Hashtable<String, YeelightDevice> devices; byte[] buffer = new byte[BUFFER_LENGTH]; DatagramPacket dgram = new DatagramPacket(buffer, buffer.length); /** * The BundleContext. This is only valid when the bundle is ACTIVE. It is set in the activate() * method and must not be accessed anymore once the deactivate() method was called or before activate() * was called. */ private BundleContext bundleContext; private ItemRegistry itemRegistry; /** * the refresh interval which is used to poll values from the Yeelight * server (optional, defaults to 60000ms) */ private long refreshInterval = 60000; public void setItemRegistry(ItemRegistry itemRegistry) { this.itemRegistry = itemRegistry; } public void unsetItemRegistry(ItemRegistry itemRegistry) { this.itemRegistry = null; } public YeelightBinding() { } /** * Called by the SCR to activate the component with its configuration read from CAS * * @param bundleContext BundleContext of the Bundle that defines this component * @param configuration Configuration properties for this component obtained from the ConfigAdmin service */ public void activate(final BundleContext bundleContext, final Map<String, Object> configuration) { this.bundleContext = bundleContext; // the configuration is guaranteed not to be null, because the component definition has the // configuration-policy set to require. If set to 'optional' then the configuration may be null // to override the default refresh interval one has to add a // parameter to openhab.cfg like <bindingName>:refresh=<intervalInMs> String refreshIntervalString = (String) configuration.get("refresh"); if (StringUtils.isNotBlank(refreshIntervalString)) { refreshInterval = Long.parseLong(refreshIntervalString); } devices = new Hashtable<>(); setupSocket(); setProperlyConfigured(socket != null); } private void setupSocket() { try { socket = new MulticastSocket(); // must bind receive side socket.joinGroup(InetAddress.getByName(MCAST_ADDR)); } catch (IOException e) { logger.error(e.toString()); } thread = new Thread(new Runnable() { public void run() { receiveData(socket, dgram); } }); thread.start(); } private void receiveData(MulticastSocket socket, DatagramPacket dgram) { try { while (true) { socket.receive(dgram); String sentence = new String(dgram.getData(), 0, dgram.getLength()); logger.debug("Yeelight received packet: {}", sentence); if (isOKPacket(sentence) || isNotifyPacket(sentence)) { String[] lines = sentence.split("\n"); String id = ""; String location = ""; String model = ""; String support = ""; for (String line : lines) { line = line.replace("\r", ""); line = line.replace("\n", ""); if (line.startsWith("id: ")) id = line.substring(4); else if (line.startsWith("Location: ")) location = line.substring(10); else if (line.startsWith("model: ")) model = line.substring(7); else if (line.startsWith("support: ")) support = line.substring(9); } if (!id.equals("") && !devices.containsKey(id)) { YeelightDevice device = new YeelightDevice(id, location, model, support); devices.put(id, device); logger.info("Found Yeelight device :\n{}", device.toString()); } } } } catch (IOException e) { logger.error(e.toString()); } } private boolean isNotifyPacket(String sentence) { return sentence.startsWith("NOTIFY * HTTP/1.1"); } private boolean isOKPacket(String sentence) { return sentence.startsWith("HTTP/1.1 200 OK"); } private void discoverYeelightDevices() { String url = null; try { StringBuilder sb = new StringBuilder(); sb.append("M-SEARCH * HTTP/1.1\r\n"); sb.append("MAN: \"ssdp:discover\"\r\n"); sb.append("ST: wifi_bulb\r\n"); byte[] sendData = sb.toString().getBytes("UTF-8"); InetAddress addr = InetAddress.getByName(MCAST_ADDR); DatagramPacket sendPacket = new DatagramPacket(sendData, sendData.length, addr, MCAST_PORT); socket.send(sendPacket); } catch (MalformedURLException e) { logger.error("The URL '{}' is malformed: ", url, e); } catch (Exception e) { logger.error("Cannot get Yeelight login cookie: ", e); } } /** * Called by the SCR when the configuration of a binding has been changed through the ConfigAdmin service. * * @param configuration Updated configuration properties */ public void modified(final Map<String, Object> configuration) { // update the internal configuration accordingly } /** * Called by the SCR to deactivate the component when either the configuration is removed or * mandatory references are no longer satisfied or the component has simply been stopped. * * @param reason Reason code for the deactivation:<br> * <ul> * <li> 0 Unspecified * <li> 1 The component was disabled * <li> 2 A reference became unsatisfied * <li> 3 A configuration was changed * <li> 4 A configuration was deleted * <li> 5 The component was disposed * <li> 6 The bundle was stopped * </ul> */ public void deactivate(final int reason) { this.bundleContext = null; if (thread != null && thread.isAlive()) thread.interrupt(); if (this.socket != null) socket.close(); devices.clear(); } /** * @{inheritDoc} */ @Override protected long getRefreshInterval() { return refreshInterval; } /** * @{inheritDoc} */ @Override protected String getName() { return "Yeelight Refresh Service"; } /** * @{inheritDoc} */ @Override protected void execute() { // the frequently executed code (polling) goes here ... logger.debug("execute() method is called!"); if (!bindingsExist()) { return; } Hashtable<String, YeelightGetPropsResponse> propList = new Hashtable<>(); //devices.clear(); discoverYeelightDevices(); for (final YeelightBindingProvider provider : providers) { for (String itemName : provider.getItemNames()) { YeelightBindingConfig config = (YeelightBindingConfig) provider.getItemConfig(itemName); if (config == null) continue; String action = config.getAction(); if (action.equals(TOGGLE)) continue; String location = config.getLocation(); YeelightGetPropsResponse result; if (!propList.containsKey(location)) { result = sendYeelightGetPropCommand(location); if (result == null) continue; propList.put(location, result); logger.debug("Cached location: {}", location); } else { result = propList.get(location); } processYeelightResult(result, action, itemName); } } } private void processYeelightResult(YeelightGetPropsResponse result, String action, String itemName) { State newState = null; State oldState = null; try { switch (action) { case SET_POWER: String power = result.getResult().get(0); newState = power.equals("on") ? OnOffType.ON : OnOffType.OFF; break; case SET_BRIGHT: int bright = result.getResult().get(1).isEmpty() ? 0 : Integer.parseInt(result.getResult().get(1)); newState = new PercentType(bright == 1 ? 0 : bright); break; case SET_CT: int ct = result.getResult().get(2).isEmpty() ? 0 : Integer.parseInt(result.getResult().get(2)); ; newState = new PercentType((ct - 1700) / 48); break; case SET_HSB: int hue = result.getResult().get(3).isEmpty() ? 0 : Integer.parseInt(result.getResult().get(3)); int sat = result.getResult().get(4).isEmpty() ? 0 : Integer.parseInt(result.getResult().get(4)); int br = result.getResult().get(1).isEmpty() ? 0 : Integer.parseInt(result.getResult().get(1)); newState = new HSBType(new DecimalType(hue), new PercentType(sat), new PercentType(br == 1 ? 0 : br)); break; case SET_RGB: int rgb = result.getResult().get(5).isEmpty() ? 0 : Integer.parseInt(result.getResult().get(5)); Color col = getRGBColor(rgb); newState = new HSBType(col); break; case NIGHTLIGHT: String status = result.getResult().get(6); newState = (status.equals("0") || status.equals("")) ? OnOffType.OFF : OnOffType.ON; break; default: logger.error("Unknown Yeelight action: {}", action); } oldState = itemRegistry.getItem(itemName).getState(); } catch (ItemNotFoundException e) { logger.error(e.toString()); } if (oldState == null || !oldState.equals(newState)) { eventPublisher.postUpdate(itemName, newState); } } /** * @{inheritDoc} */ @Override protected void internalReceiveCommand(String itemName, Command command) { // the code being executed when a command was sent on the openHAB // event bus goes here. This method is only called if one of the // BindingProviders provide a binding for the given 'itemName'. logger.debug("internalReceiveCommand({},{}) is called!", itemName, command); YeelightBindingConfig config = getItemConfig(itemName); if (config == null) return; String location = config.getLocation(); String action = config.getAction(); switch (action) { case SET_POWER: if (command instanceof OnOffType) { sendYeelightPowerCommand(location, command.toString().toLowerCase()); } break; case NIGHTLIGHT: if (command instanceof OnOffType) { sendYeelightNightModeCommand(location, command.equals(OnOffType.ON)); } break; case TOGGLE: if (command instanceof OnOffType && command.equals(OnOffType.ON)) { sendYeelightToggleCommand(location); } break; case SET_BRIGHT: sendYeelightBrightCommand(location, Integer.parseInt(command.toString())); break; case SET_CT: sendYeelightCTCommand(location, 1700 + 48 * Integer.parseInt(command.toString())); break; case SET_HSB: if (command instanceof HSBType) { HSBType hsb = (HSBType) command; sendYeelightHSCommand(location, hsb.getHue().intValue(), hsb.getSaturation().intValue()); sendYeelightBrightCommand(location, hsb.getBrightness().intValue()); } else if (command instanceof OnOffType) { sendYeelightPowerCommand(location, command.toString().toLowerCase()); } break; case SET_RGB: if (command instanceof HSBType) { HSBType hsb = (HSBType) command; sendYeelightRGBCommand(location, hsb.getRed().intValue(), hsb.getGreen().intValue(), hsb.getBlue().intValue()); sendYeelightBrightCommand(location, hsb.getBrightness().intValue()); } else if (command instanceof OnOffType) { sendYeelightPowerCommand(location, command.toString().toLowerCase()); } break; default: logger.error("Unknown Yeelight command: {}", action); } } private YeelightGetPropsResponse sendYeelightGetPropCommand(String location) { String result = sendYeelightCommand(location, GET_PROP, new Object[] { "power", "bright", "ct", "hue", "sat", "rgb", "nl_br" }); logger.debug("location: {}, props: {}", location, result); return gson.fromJson(result, YeelightGetPropsResponse.class); } private String sendYeelightToggleCommand(String location) { return sendYeelightCommand(location, TOGGLE, new Object[] {}); } private String sendYeelightBrightCommand(String location, int param) { return sendYeelightCommand(location, SET_BRIGHT, new Object[] { param == 0 ? 1 : param, SMOOTH, 500 }); } private String sendYeelightNightModeCommand(String location, boolean mode) { if (mode) { //return sendYeelightCommand(location, SET_SCENE, new Object[]{NIGHTLIGHT, 1}); return sendYeelightPowerCommand(location, "on", 5); } else { return sendYeelightPowerCommand(location, "on", 1); } } private String sendYeelightRGBCommand(String location, int red, int green, int blue) { return sendYeelightCommand(location, SET_RGB, new Object[] { getRGBValue(red, green, blue), SMOOTH, 500 }); } private String sendYeelightHSCommand(String location, int hue, int saturation) { return sendYeelightCommand(location, "set_hsv", new Object[] { hue, saturation, SMOOTH, 500 }); } private String sendYeelightCTCommand(String location, int param) { return sendYeelightCommand(location, "set_ct_abx", new Object[] { param, SMOOTH, 500 }); } private String sendYeelightPowerCommand(String location, String param) { return sendYeelightCommand(location, SET_POWER, new Object[] { param, "", 0 }); } private String sendYeelightPowerCommand(String location, String param, int mode) { return sendYeelightCommand(location, SET_POWER, new Object[] { param, "", 0, mode }); } private String sendYeelightCommand(String location, String action, Object[] params) { int index = location.indexOf(":"); Socket clientSocket = null; try { String ip = location.substring(0, index); int port = Integer.parseInt(location.substring(index + 1)); clientSocket = new Socket(ip, port); DataOutputStream outToServer = new DataOutputStream(clientSocket.getOutputStream()); BufferedReader inFromServer = new BufferedReader(new InputStreamReader(clientSocket.getInputStream())); String sentence = "{\"id\":" + msgid++ + ",\"method\":\"" + action + "\",\"params\":[" + getProperties(params) + "]}\r\n"; logger.debug("Sending sentence: {}", sentence); outToServer.writeBytes(sentence); return inFromServer.readLine(); } catch (NoRouteToHostException e) { logger.debug("Location {} is probably offline", location); if (action.equals(GET_PROP)) { //update switches if not found location to OFF state for (Object item : getOnSwitchItems(location)) { eventPublisher.postUpdate((String) item, OnOffType.OFF); } } } catch (IOException e) { logger.error(e.toString()); } finally { if (clientSocket != null) try { clientSocket.close(); } catch (IOException e) { //silence } } return null; } private Object[] getOnSwitchItems(String location) { ArrayList<String> list = new ArrayList<>(); for (final YeelightBindingProvider provider : providers) { for (String itemName : provider.getItemNames()) { YeelightBindingConfig config = (YeelightBindingConfig) provider.getItemConfig(itemName); String action = config.getAction(); if (config.getLocation().equals(location) && (action.equals(SET_POWER) || action.equals(NIGHTLIGHT))) { try { State oldState = itemRegistry.getItem(itemName).getState(); if (oldState.equals(OnOffType.ON)) { list.add(itemName); } } catch (ItemNotFoundException e) { logger.error(e.toString()); } } } } return list.toArray(); } private int getRGBValue(int red, int green, int blue) { return red * 65536 + green * 256 + blue; } private Color getRGBColor(int rgb) { int red = rgb / 65536; int green = (rgb - red * 65536) / 256; int blue = rgb - red * 65536 - green * 256; return new Color(red, green, blue); } private YeelightBindingConfig getItemConfig(String itemName) { for (final YeelightBindingProvider provider : providers) { if (provider.getItemNames().contains(itemName)) return (YeelightBindingConfig) provider.getItemConfig(itemName); } return null; } private String getProperties(Object[] properties) { StringBuilder builder = new StringBuilder(); boolean first = true; for (Object o : properties) { if (!first) builder.append(","); else first = false; if (o instanceof String) { builder.append("\""); builder.append(o); builder.append("\""); } else builder.append(o); } return builder.toString(); } /** * @{inheritDoc} */ @Override protected void internalReceiveUpdate(String itemName, State newState) { // the code being executed when a state was sent on the openHAB // event bus goes here. This method is only called if one of the // BindingProviders provide a binding for the given 'itemName'. logger.debug("internalReceiveUpdate({},{}) is called!", itemName, newState); } }