/** * Copyright (c) 2010-2018 by the respective copyright holders. * * 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.heos.handler; import static org.openhab.binding.heos.HeosBindingConstants.*; import static org.openhab.binding.heos.internal.resources.HeosConstants.*; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; import org.eclipse.smarthome.config.discovery.DiscoveryListener; import org.eclipse.smarthome.config.discovery.DiscoveryResult; import org.eclipse.smarthome.config.discovery.DiscoveryService; import org.eclipse.smarthome.core.library.types.OnOffType; import org.eclipse.smarthome.core.thing.Bridge; import org.eclipse.smarthome.core.thing.Channel; import org.eclipse.smarthome.core.thing.ChannelUID; import org.eclipse.smarthome.core.thing.Thing; import org.eclipse.smarthome.core.thing.ThingStatus; import org.eclipse.smarthome.core.thing.ThingTypeUID; import org.eclipse.smarthome.core.thing.ThingUID; import org.eclipse.smarthome.core.thing.binding.BaseBridgeHandler; import org.eclipse.smarthome.core.thing.binding.ThingHandler; import org.eclipse.smarthome.core.thing.binding.builder.ChannelBuilder; import org.eclipse.smarthome.core.thing.binding.builder.ThingBuilder; import org.eclipse.smarthome.core.thing.type.ChannelTypeUID; import org.eclipse.smarthome.core.types.Command; import org.eclipse.smarthome.core.types.RefreshType; import org.openhab.binding.heos.internal.HeosChannelHandlerFactory; import org.openhab.binding.heos.internal.api.HeosFacade; import org.openhab.binding.heos.internal.api.HeosSystem; import org.openhab.binding.heos.internal.discovery.HeosPlayerDiscovery; import org.openhab.binding.heos.internal.handler.HeosChannelHandler; import org.openhab.binding.heos.internal.resources.HeosEventListener; import org.openhab.binding.heos.internal.resources.HeosGroup; import org.openhab.binding.heos.internal.resources.HeosPlayer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * The {@link HeosSystemHandler} is responsible for handling commands, which are * sent to one of the channels. * * @author Johannes Einig - Initial contribution */ public class HeosBridgeHandler extends BaseBridgeHandler implements HeosEventListener, DiscoveryListener { private List<String> heosPlaylists = new ArrayList<String>(); private HashMap<ThingUID, ThingHandler> handlerList = new HashMap<>(); private HashMap<String, String> selectedPlayer = new HashMap<String, String>(); private ArrayList<String[]> selectedPlayerList = new ArrayList<String[]>(); private HashMap<ThingUID, ThingStatus> thingOnlineState = new HashMap<ThingUID, ThingStatus>(); private HeosChannelHandlerFactory channelHandlerFactory = null; private ScheduledExecutorService initPhaseExecutor; private InitProcedure initPhaseRunnable = new InitProcedure(); private HeosPlayerDiscovery playerDiscovery; private HeosSystem heos; private HeosFacade api; private int heartbeatPulse = 0; private boolean isRegisteredForChangeEvents = false; private boolean bridgeIsConnected = false; private boolean handleGroups = true; private boolean loggedIn = false; private boolean connectionDelay = false; private boolean bridgeHandlerdisposalOngoing = false; private Logger logger = LoggerFactory.getLogger(HeosBridgeHandler.class); public HeosBridgeHandler(Bridge thing, HeosSystem heos, HeosFacade api) { super(thing); this.heos = heos; this.api = api; channelHandlerFactory = new HeosChannelHandlerFactory(this, api); } @Override public void handleCommand(@NonNull ChannelUID channelUID, @NonNull Command command) { if (command instanceof RefreshType) { return; } HeosChannelHandler channelHandler = channelHandlerFactory.getChannelHandler(channelUID); if (channelHandler != null) { channelHandler.handleCommand(command, this, channelUID); return; } } public void resetPlayerList(@NonNull ChannelUID channelUID) { for (int i = 0; i < selectedPlayerList.size(); i++) { updateState(selectedPlayerList.get(i)[1], OnOffType.OFF); } selectedPlayerList.clear(); updateState(channelUID, OnOffType.OFF); } @Override public synchronized void initialize() { if (bridgeIsConnected) { return; } loggedIn = false; logger.info("Initialize Bridge '{}' with IP '{}'", thing.getConfiguration().get(NAME), thing.getConfiguration().get(HOST)); heartbeatPulse = Integer.valueOf(thing.getConfiguration().get(HEARTBEAT).toString()); heos.setConnectionIP(thing.getConfiguration().get(HOST).toString()); heos.setConnectionPort(1255); bridgeIsConnected = heos.establishConnection(connectionDelay); // the connectionDelay gives the HEOS time to // recover after a restart while (!bridgeIsConnected) { heos.closeConnection(); bridgeIsConnected = heos.establishConnection(connectionDelay); logger.warn("Could not initialize connection to HEOS system"); } if (!isRegisteredForChangeEvents) { api.registerforChangeEvents(this); isRegisteredForChangeEvents = true; } scheduledStartUp(); handleGroups = true; updateStatus(ThingStatus.ONLINE); updateState(CH_ID_REBOOT, OnOffType.OFF); logger.info("HEOS bridge Online"); connectionDelay = false; // sets default to false again } @Override public void dispose() { bridgeHandlerdisposalOngoing = true; // Flag to prevent the handler from being updated during disposal api.unregisterforChangeEvents(this); logger.info("HEOS bridge removed from change notifications"); isRegisteredForChangeEvents = false; loggedIn = false; logger.info("Dispose bridge '{}'", thing.getConfiguration().get(NAME)); heos.closeConnection(); bridgeIsConnected = false; initPhaseExecutor.shutdownNow(); // Prevents doubled execution if OpenHab doubles initialization of the // bridge // updateStatus(ThingStatus.OFFLINE); super.dispose(); } /** * Manages the adding of the childHandler to the handlerList and sets the Status * of the thing to ThingStatus.ONLINE. * Add also the player or group channel to the bridge. */ @Override public synchronized void childHandlerInitialized(ThingHandler childHandler, Thing childThing) { handlerList.put(childThing.getUID(), childHandler); thingOnlineState.put(childThing.getUID(), ThingStatus.ONLINE); this.addPlayerChannel(childThing); logger.info("Initzialize child handler for: {}.", childThing.getUID().getId()); } /** * Manages the removal of the childHandler from the handlerList and sets the Status * of the thing to ThingsStatus.REMOVED * Removes also the channel of the player or group from the bridge. * */ @Override public synchronized void childHandlerDisposed(ThingHandler childHandler, Thing childThing) { try { Thread.sleep(500); } catch (InterruptedException e) { logger.debug("Interrupted Exection - Message: {}", e.getMessage()); } if (bridgeHandlerdisposalOngoing) { // Checks if bridgeHandler is going to disposed (by stopping the binding or // OpenHab for example) and prevents it from being updated which stops the // disposal process. return; } else if (childThing.getConfiguration().get(TYPE).equals(PLAYER)) { String channelIdentifyer = "P" + childThing.getConfiguration().get(PID).toString(); this.removeChannel(CH_TYPE_PLAYER, channelIdentifyer); } else { String channelIdentifyer = "G" + childThing.getConfiguration().get(GID).toString(); this.removeChannel(CH_TYPE_PLAYER, channelIdentifyer); } handlerList.remove(childThing.getUID(), childHandler); thingOnlineState.put(childThing.getUID(), ThingStatus.REMOVED); logger.info("Dispose child handler for: {}.", childThing.getUID().getId()); return; } public void setThingStatusOffline(ThingUID uid) { thingOnlineState.put(uid, ThingStatus.OFFLINE); } public void setThingStatusOnline(ThingUID uid) { thingOnlineState.put(uid, ThingStatus.ONLINE); } public void setHeosPlayerDiscovery(HeosPlayerDiscovery discover) { this.playerDiscovery = discover; } @Override public void playerStateChangeEvent(String pid, String event, String command) { // Do nothing } @Override public void playerMediaChangeEvent(String pid, HashMap<String, String> info) { // Do nothing } @Override public void bridgeChangeEvent(String event, String result, String command) { if (event.equals(EVENTTYPE_EVENT)) { if (command.equals(PLAYERS_CHANGED)) { playerDiscovery.scanForNewPlayers(); } else if (command.equals(GROUPS_CHANGED)) { playerDiscovery.scanForNewPlayers(); } else if (command.equals(CONNECTION_LOST)) { updateStatus(ThingStatus.OFFLINE); bridgeIsConnected = false; logger.warn("Heos Bridge OFFLINE"); } else if (command.equals(CONNECTION_RESTORED)) { connectionDelay = true; initialize(); } } if (event.equals(EVENTTYPE_SYSTEM)) { if (command.equals(SING_IN)) { if (result.equals(SUCCESS)) { if (!loggedIn) { loggedIn = true; addFavorits(); addPlaylists(); } } } else if (command.equals(USER_CHANGED)) { if (!loggedIn) { loggedIn = true; addFavorits(); addPlaylists(); } } } } /** * If a thing is discovered, the method checks if the thing is already known * and the state is only temporary set to OFFLINE. If so initialize() of the * thing is called. */ @Override public void thingDiscovered(DiscoveryService source, DiscoveryResult result) { if (handlerList.containsKey(result.getThingUID())) { if (thingOnlineState.containsKey(result.getThingUID())) { if (thingOnlineState.get(result.getThingUID()).equals(ThingStatus.OFFLINE)) { handlerList.get(result.getThingUID()).initialize(); } } } } /** * If handleGroups is activated, the HEOS group is not removed as things. * Only the status is set to offline and the information is stored within * the thingOnlineState HashMap as ThingStatus.OFFLINE. * If handleGroups is not active the thing is completely removed. The handler * is removed from the handler list via childHandlerDisposed() */ @Override public void thingRemoved(DiscoveryService source, ThingUID thingUID) { logger.info("Removing Thing: {}.", thingUID.getId()); if (handlerList.get(thingUID) != null) { if (!handleGroups) { handlerList.get(thingUID).handleRemoval(); } else { if (handlerList.get(thingUID).getClass().equals(HeosGroupHandler.class)) { HeosGroupHandler handler = (HeosGroupHandler) handlerList.get(thingUID); thingOnlineState.put(thingUID, ThingStatus.OFFLINE); handler.setStatusOffline(); } else if (handlerList.get(thingUID).getClass().equals(HeosPlayerHandler.class)) { HeosPlayerHandler handler = (HeosPlayerHandler) handlerList.get(thingUID); thingOnlineState.put(thingUID, ThingStatus.OFFLINE); handler.setStatusOffline(); } } } } @Override public @NonNull Collection<@NonNull ThingUID> removeOlderResults(@NonNull DiscoveryService source, long timestamp, @Nullable Collection<@NonNull ThingTypeUID> thingTypeUIDs, @Nullable ThingUID bridgeUID) { return Collections.emptyList(); } public void addPlaylists() { if (loggedIn) { heosPlaylists.clear(); heosPlaylists = heos.getPlaylists(); } } public void addFavorits() { if (loggedIn) { logger.info("Adding HEOS Favorite Channels"); removeChannels(CH_TYPE_FAVORIT); logger.info("Old Favorite Channels removed"); List<HashMap<String, String>> favList = new ArrayList<HashMap<String, String>>(); HashMap<String, String> favorits = new HashMap<String, String>(4); favList = heos.getFavorits(); int favCount = favList.size(); ArrayList<Channel> favoritChannels = new ArrayList<Channel>(favCount); if (favCount != 0) { for (int i = 0; i < favCount; i++) { for (String key : favList.get(i).keySet()) { if (key.equals(MID)) { favorits.put(key, favList.get(i).get(key)); } if (key.equals(NAME)) { favorits.put(key, favList.get(i).get(key)); } if (key.equals("null")) { return; } } logger.info("Add Favorite Channel: {}", favorits.get(NAME)); favoritChannels.add(createFavoritChannel(favorits)); } } addChannel(favoritChannels); } } /** * Create a channel for the childThing. Depending if it is a HEOS Group * or a player an identification prefix is added * * @param childThing the thing the channel is created for */ private void addPlayerChannel(Thing childThing) { String channelIdentifyer = ""; String pid = ""; if (childThing.getConfiguration().get(TYPE).equals(PLAYER)) { channelIdentifyer = "P" + childThing.getConfiguration().get(PID).toString(); pid = childThing.getConfiguration().get(PID).toString(); } else if (childThing.getConfiguration().get(TYPE).equals(GROUP)) { channelIdentifyer = "G" + childThing.getConfiguration().get(GID).toString(); pid = childThing.getConfiguration().get(GID).toString(); } @NonNull String playerName = childThing.getConfiguration().get(NAME).toString(); ChannelUID channelUID = new ChannelUID(this.getThing().getUID(), channelIdentifyer); if (!hasChannel(channelUID)) { HashMap<String, String> properties = new HashMap<String, String>(2); properties.put(NAME, childThing.getConfiguration().get(NAME).toString()); properties.put(PID, pid); Channel channel = ChannelBuilder.create(channelUID, "Switch").withLabel(playerName).withType(CH_TYPE_PLAYER) .withProperties(properties).build(); ArrayList<Channel> newChannelList = new ArrayList<>(1); newChannelList.add(channel); addChannel(newChannelList); } } private void addChannel(List<Channel> newChannelList) { List<Channel> existingChannelList = thing.getChannels(); ArrayList<Channel> mutableChannelList = new ArrayList<Channel>(); mutableChannelList.addAll(existingChannelList); mutableChannelList.addAll(newChannelList); ThingBuilder thingBuilder = editThing(); thingBuilder.withChannels(mutableChannelList); updateThing(thingBuilder.build()); } private Channel createFavoritChannel(HashMap<String, String> properties) { String favoritName = properties.get(NAME); Channel channel = ChannelBuilder.create(new ChannelUID(this.getThing().getUID(), properties.get(MID)), "Switch") .withLabel(favoritName).withType(CH_TYPE_FAVORIT).withProperties(properties).build(); return channel; } private void removeChannels(ChannelTypeUID channelType) { List<Channel> channelList = thing.getChannels(); ArrayList<Channel> mutableChannelList = new ArrayList<Channel>(); mutableChannelList.addAll(channelList); for (int i = 0; i < mutableChannelList.size(); i++) { if (mutableChannelList.get(i).getChannelTypeUID().equals(channelType)) { mutableChannelList.remove(i); i = 0; } } ThingBuilder thingBuilder = editThing(); thingBuilder.withChannels(mutableChannelList); updateThing(thingBuilder.build()); } private void removeChannel(ChannelTypeUID channelType, String channelIdentifyer) { ChannelUID channelUID = new ChannelUID(this.thing.getUID(), channelIdentifyer); List<Channel> channelList = thing.getChannels(); ArrayList<Channel> mutableChannelList = new ArrayList<Channel>(); mutableChannelList.addAll(channelList); for (int i = 0; i < mutableChannelList.size(); i++) { if (mutableChannelList.get(i).getUID().equals(channelUID)) { mutableChannelList.remove(i); } } ThingBuilder thingBuilder = editThing(); thingBuilder.withChannels(mutableChannelList); updateThing(thingBuilder.build()); } private boolean hasChannel(ChannelUID channelUID) { List<Channel> channelList = thing.getChannels(); for (int i = 0; i < channelList.size(); i++) { if (channelList.get(i).getUID().equals(channelUID)) { return true; } } return false; } public HashMap<String, HeosPlayer> getNewPlayer() { return heos.getAllPlayer(); } public HashMap<String, HeosGroup> getNewGroups() { return heos.getGroups(); } public HashMap<String, HeosGroup> getRemovedGroups() { return heos.getGroupsRemoved(); } public HashMap<String, HeosPlayer> getRemovedPlayer() { return heos.getPlayerRemoved(); } /** * The list with the currently selected player * * @return a HashMap which the currently selected player */ public HashMap<String, String> getSelectedPlayer() { selectedPlayer.clear(); for (int i = 0; i < selectedPlayerList.size(); i++) { selectedPlayer.put(selectedPlayerList.get(i)[0], selectedPlayerList.get(i)[1]); } return selectedPlayer; } public void setSelectedPlayer(HashMap<String, String> selectedPlayer) { this.selectedPlayer = selectedPlayer; } /** * @return the selectedPlayerList */ public ArrayList<String[]> getSelectedPlayerList() { return selectedPlayerList; } /** * @return the selectedPlayerList */ public void setSelectedPlayerList(ArrayList<String[]> selectedPlayerList) { this.selectedPlayerList = selectedPlayerList; } /** * @return the heosPlaylists */ public List<String> getHeosPlaylists() { return heosPlaylists; } /** * @return the channelHandlerFactory */ public HeosChannelHandlerFactory getChannelHandlerFactory() { return channelHandlerFactory; } /** * @param channelHandlerFactory the channelHandlerFactory to set */ public void setChannelHandlerFactory(HeosChannelHandlerFactory channelHandlerFactory) { this.channelHandlerFactory = channelHandlerFactory; } /** * @return the handleGroups */ public boolean isHandleGroups() { return handleGroups; } /** * @param handleGroups the handleGroups to set */ public void setHandleGroups(boolean handleGroups) { this.handleGroups = handleGroups; } private void scheduledStartUp() { initPhaseExecutor = Executors.newScheduledThreadPool(1); initPhaseRunnable = new InitProcedure(); initPhaseExecutor.schedule(this.initPhaseRunnable, 10, TimeUnit.SECONDS); } public class InitProcedure implements Runnable { @Override public void run() { bridgeHandlerdisposalOngoing = false; heos.startEventListener(); heos.startHeartBeat(heartbeatPulse); logger.info("HEOS System heart beat started. Pulse time is {}s", heartbeatPulse); updateState(CH_ID_DYNGROUPSHAND, OnOffType.ON); // activates dynamic group handling by default if (thing.getConfiguration().containsKey(USERNAME) && thing.getConfiguration().containsKey(PASSWORD)) { logger.info("Logging in to HEOS account."); String name = thing.getConfiguration().get(USERNAME).toString(); String password = thing.getConfiguration().get(PASSWORD).toString(); api.logIn(name, password); } else { logger.warn("Can not log in. Username and Password not set"); } } } }