2 * Copyright (c) 2010-2021 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.russound.internal.rio;
15 import java.util.ArrayList;
16 import java.util.List;
17 import java.util.concurrent.CopyOnWriteArrayList;
18 import java.util.concurrent.locks.Lock;
19 import java.util.concurrent.locks.ReentrantLock;
20 import java.util.regex.Matcher;
21 import java.util.regex.Pattern;
23 import org.apache.commons.lang.StringUtils;
24 import org.openhab.binding.russound.internal.net.SocketSession;
25 import org.openhab.binding.russound.internal.net.SocketSessionListener;
26 import org.openhab.binding.russound.internal.rio.models.GsonUtilities;
27 import org.openhab.binding.russound.internal.rio.models.RioFavorite;
28 import org.slf4j.Logger;
29 import org.slf4j.LoggerFactory;
31 import com.google.gson.Gson;
32 import com.google.gson.JsonSyntaxException;
35 * This {@link AbstractRioProtocol} implementation provides the implementation for managing Russound system favorites.
36 * Since refreshing all 32 system favorites requires 64 calls to russound (for name/valid), we limit how often we can
37 * refresh to {@link #UPDATE_TIME_SPAN}.
39 * @author Tim Roberts - Initial contribution
41 public class RioSystemFavoritesProtocol extends AbstractRioProtocol {
44 private final Logger logger = LoggerFactory.getLogger(RioSystemFavoritesProtocol.class);
46 // Helper names in the protocol
47 private static final String FAV_NAME = "name";
48 private static final String FAV_VALID = "valid";
51 * The pattern representing system favorite notifications
53 private static final Pattern RSP_SYSTEMFAVORITENOTIFICATION = Pattern
54 .compile("(?i)^[SN] System.favorite\\[(\\d+)\\].(\\w+)=\"(.*)\"$");
57 * The current state of all 32 system favorites
59 private final RioFavorite[] systemFavorites = new RioFavorite[32];
62 * The {@link Gson} used for all JSON operations
64 private final Gson gson;
67 * The {@link ReentrantLock} used to control access to {@link #lastUpdateTime}
69 private final Lock lastUpdateLock = new ReentrantLock();
72 * The last time we did a full refresh of system favorites via {@link #refreshSystemFavorites()}
74 private long lastUpdateTime;
77 * The minimum timespan between full refreshes of system favorites (via {@link #refreshSystemFavorites()})
79 private static final long UPDATE_TIME_SPAN = 60000;
82 * The list of listeners that will be called when system favorites have changed
84 private final CopyOnWriteArrayList<Listener> listeners = new CopyOnWriteArrayList<>();
87 * Constructs the system favorite protocol from the given session and callback. Note: the passed callback is not
90 * @param session a non null {@link SocketSession} to use
91 * @param callback a non-null {@link RioHandlerCallback} to use
93 public RioSystemFavoritesProtocol(SocketSession session, RioHandlerCallback callback) {
94 super(session, callback);
96 gson = GsonUtilities.createGson();
98 for (int x = 1; x <= 32; x++) {
99 systemFavorites[x - 1] = new RioFavorite(x);
104 * Adds the specified listener to changes in system favorites
106 * @param listener a non-null listener to add
107 * @throws IllegalArgumentException if listener is null
109 public void addListener(Listener listener) {
110 if (listener == null) {
111 throw new IllegalArgumentException("listener cannot be null");
113 listeners.add(listener);
117 * Removes the specified listener from change notifications
119 * @param listener a possibly null listener to remove (null is ignored)
120 * @return true if removed, false otherwise
122 public boolean removeListener(Listener listener) {
123 return listeners.remove(listener);
127 * Fires the systemFavoritesUpdated method on all listeners with the results of {@link #getJson()}
129 private void fireUpdate() {
130 final String json = getJson();
131 for (Listener l : listeners) {
132 l.systemFavoritesUpdated(json);
137 * Helper method to request the specified system favorite id information (name/valid). Please note that this does
138 * NOT change the {@link #lastUpdateTime}
140 * @param favIds a non-null, possibly empty list of system favorite ids to request (any id < 1 or > 32 will be
142 * @throws IllegalArgumentException if favIds is null
144 private void requestSystemFavorites(List<Integer> favIds) {
145 if (favIds == null) {
146 throw new IllegalArgumentException("favIds cannot be null");
148 for (final Integer favId : favIds) {
149 if (favId >= 1 && favId <= 32) {
150 sendCommand("GET System.favorite[" + favId + "].name");
151 sendCommand("GET System.favorite[" + favId + "].valid");
157 * Refreshes ALL system favorites if they have not been refreshed within the last
158 * {@link #UPDATE_TIME_SPAN}. This method WILL change the {@link #lastUpdateTime}
160 public void refreshSystemFavorites() {
161 lastUpdateLock.lock();
163 final long now = System.currentTimeMillis();
164 if (now > lastUpdateTime + UPDATE_TIME_SPAN) {
165 lastUpdateTime = now;
166 for (int x = 1; x <= 32; x++) {
167 sendCommand("GET System.favorite[" + x + "].valid");
168 sendCommand("GET System.favorite[" + x + "].name");
172 lastUpdateLock.unlock();
177 * Returns the JSON representation of all the system favorites and their state.
179 * @return A non-null, non-empty JSON representation of {@link #systemFavorites}
181 public String getJson() {
182 final List<RioFavorite> favs = new ArrayList<>();
183 for (final RioFavorite fav : systemFavorites) {
188 return gson.toJson(favs);
192 * Sets the system favorites for a controller/zone. For each system favorite found in the favJson parameter, this
193 * method will either save the system favorite (if it's status changed from not valid to valid) or save the system
194 * favorite name (if only the name changed) or delete the system favorite (if status changed from valid to invalid).
196 * @param controller the controller number between 1 and 6
197 * @param zone the zone number between 1 and 8
198 * @param favJson the possibly empty, possibly null JSON representation of system favorites
199 * @throws IllegalArgumentException if controller is < 1 or > 6
200 * @throws IllegalArgumentException if zone is < 1 or > 8
202 public void setSystemFavorites(int controller, int zone, String favJson) {
203 if (controller < 1 || controller > 6) {
204 throw new IllegalArgumentException("Controller must be between 1 and 6");
207 if (zone < 1 || zone > 8) {
208 throw new IllegalArgumentException("Zone must be between 1 and 8");
211 if (StringUtils.isEmpty(favJson)) {
215 final List<Integer> updateFavIds = new ArrayList<>();
217 final RioFavorite[] favs;
218 favs = gson.fromJson(favJson, RioFavorite[].class);
219 for (int x = favs.length - 1; x >= 0; x--) {
220 final RioFavorite fav = favs[x];
222 continue; // caused by {id,valid,name},,{id,valid,name}
225 final int favId = fav.getId();
226 if (favId < 1 || favId > 32) {
227 logger.debug("Invalid favorite id (not between 1 and 32) - ignoring: {}:{}", favId, favJson);
229 final RioFavorite myFav = systemFavorites[favId - 1];
230 final boolean favValid = fav.isValid();
231 final String favName = fav.getName();
233 // re-retrieve to see if the save/delete worked (saving on a zone that's off - valid won't be set to
235 if (myFav.isValid() != favValid) {
236 myFav.setValid(favValid);
238 myFav.setName(favName);
239 sendCommand("EVENT C[" + controller + "].Z[" + zone + "]!saveSystemFavorite \"" + favName
241 updateFavIds.add(favId);
243 sendCommand("EVENT C[" + controller + "].Z[" + zone + "]!deleteSystemFavorite " + favId);
245 } else if (!StringUtils.equals(myFav.getName(), favName)) {
246 myFav.setName(favName);
247 sendCommand("SET System.favorite[" + favId + "]." + FAV_NAME + "=\"" + favName + "\"");
251 } catch (JsonSyntaxException e) {
252 logger.debug("Invalid JSON: {}", e.getMessage(), e);
255 // Refresh the favorites that changed (verifies if the favorite was actually saved)
256 requestSystemFavorites(updateFavIds);
258 // Refresh any listeners immediately to reset the channel
263 * Handles any system notifications returned by the russound system
265 * @param m a non-null matcher
266 * @param resp a possibly null, possibly empty response
268 private void handleSystemFavoriteNotification(Matcher m, String resp) {
270 throw new IllegalArgumentException("m (matcher) cannot be null");
272 if (m.groupCount() == 3) {
274 final int favoriteId = Integer.parseInt(m.group(1));
276 if (favoriteId >= 1 && favoriteId <= 32) {
277 final RioFavorite fav = systemFavorites[favoriteId - 1];
279 final String key = m.group(2).toLowerCase();
280 final String value = m.group(3);
288 fav.setValid(!"false".equalsIgnoreCase(value));
293 logger.warn("Unknown system favorite notification: '{}'", resp);
297 logger.warn("Invalid System Favorite Notification (favorite < 1 or > 32): '{}')", resp);
299 } catch (NumberFormatException e) {
300 logger.warn("Invalid System Favorite Notification (favorite not a parsable integer): '{}')", resp);
303 logger.warn("Invalid System Notification response: '{}'", resp);
308 * Implements {@link SocketSessionListener#responseReceived(String)} to try to process the response from the
309 * russound system. This response may be for other protocol handler - so ignore if we don't recognize the response.
311 * @param a possibly null, possibly empty response
314 public void responseReceived(String response) {
315 if (StringUtils.isEmpty(response)) {
319 final Matcher m = RSP_SYSTEMFAVORITENOTIFICATION.matcher(response);
321 handleSystemFavoriteNotification(m, response);
326 * Defines the listener implementation to list for system favorite updates
328 * @author Tim Roberts
331 public interface Listener {
333 * Called when system favorites have changed. The jsonString will contain the current representation of all
334 * valid system favorites.
336 * @param jsonString a non-null, non-empty json representation of {@link RioFavorite}
338 void systemFavoritesUpdated(String jsonString);