2 * Copyright (c) 2010-2023 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.Objects;
18 import java.util.concurrent.CopyOnWriteArrayList;
19 import java.util.concurrent.locks.Lock;
20 import java.util.concurrent.locks.ReentrantLock;
21 import java.util.regex.Matcher;
22 import java.util.regex.Pattern;
24 import org.eclipse.jdt.annotation.Nullable;
25 import org.openhab.binding.russound.internal.net.SocketSession;
26 import org.openhab.binding.russound.internal.net.SocketSessionListener;
27 import org.openhab.binding.russound.internal.rio.models.GsonUtilities;
28 import org.openhab.binding.russound.internal.rio.models.RioFavorite;
29 import org.slf4j.Logger;
30 import org.slf4j.LoggerFactory;
32 import com.google.gson.Gson;
33 import com.google.gson.JsonSyntaxException;
36 * This {@link AbstractRioProtocol} implementation provides the implementation for managing Russound system favorites.
37 * Since refreshing all 32 system favorites requires 64 calls to russound (for name/valid), we limit how often we can
38 * refresh to {@link #UPDATE_TIME_SPAN}.
40 * @author Tim Roberts - Initial contribution
42 public class RioSystemFavoritesProtocol extends AbstractRioProtocol {
45 private final Logger logger = LoggerFactory.getLogger(RioSystemFavoritesProtocol.class);
47 // Helper names in the protocol
48 private static final String FAV_NAME = "name";
49 private static final String FAV_VALID = "valid";
52 * The pattern representing system favorite notifications
54 private static final Pattern RSP_SYSTEMFAVORITENOTIFICATION = Pattern
55 .compile("(?i)^[SN] System.favorite\\[(\\d+)\\].(\\w+)=\"(.*)\"$");
58 * The current state of all 32 system favorites
60 private final RioFavorite[] systemFavorites = new RioFavorite[32];
63 * The {@link Gson} used for all JSON operations
65 private final Gson gson;
68 * The {@link ReentrantLock} used to control access to {@link #lastUpdateTime}
70 private final Lock lastUpdateLock = new ReentrantLock();
73 * The last time we did a full refresh of system favorites via {@link #refreshSystemFavorites()}
75 private long lastUpdateTime;
78 * The minimum timespan between full refreshes of system favorites (via {@link #refreshSystemFavorites()})
80 private static final long UPDATE_TIME_SPAN = 60000;
83 * The list of listeners that will be called when system favorites have changed
85 private final CopyOnWriteArrayList<Listener> listeners = new CopyOnWriteArrayList<>();
88 * Constructs the system favorite protocol from the given session and callback. Note: the passed callback is not
91 * @param session a non null {@link SocketSession} to use
92 * @param callback a non-null {@link RioHandlerCallback} to use
94 public RioSystemFavoritesProtocol(SocketSession session, RioHandlerCallback callback) {
95 super(session, callback);
97 gson = GsonUtilities.createGson();
99 for (int x = 1; x <= 32; x++) {
100 systemFavorites[x - 1] = new RioFavorite(x);
105 * Adds the specified listener to changes in system favorites
107 * @param listener a non-null listener to add
108 * @throws IllegalArgumentException if listener is null
110 public void addListener(Listener listener) {
111 if (listener == null) {
112 throw new IllegalArgumentException("listener cannot be null");
114 listeners.add(listener);
118 * Removes the specified listener from change notifications
120 * @param listener a possibly null listener to remove (null is ignored)
121 * @return true if removed, false otherwise
123 public boolean removeListener(Listener listener) {
124 return listeners.remove(listener);
128 * Fires the systemFavoritesUpdated method on all listeners with the results of {@link #getJson()}
130 private void fireUpdate() {
131 final String json = getJson();
132 for (Listener l : listeners) {
133 l.systemFavoritesUpdated(json);
138 * Helper method to request the specified system favorite id information (name/valid). Please note that this does
139 * NOT change the {@link #lastUpdateTime}
141 * @param favIds a non-null, possibly empty list of system favorite ids to request (any id < 1 or > 32 will be
143 * @throws IllegalArgumentException if favIds is null
145 private void requestSystemFavorites(List<Integer> favIds) {
146 if (favIds == null) {
147 throw new IllegalArgumentException("favIds cannot be null");
149 for (final Integer favId : favIds) {
150 if (favId >= 1 && favId <= 32) {
151 sendCommand("GET System.favorite[" + favId + "].name");
152 sendCommand("GET System.favorite[" + favId + "].valid");
158 * Refreshes ALL system favorites if they have not been refreshed within the last
159 * {@link #UPDATE_TIME_SPAN}. This method WILL change the {@link #lastUpdateTime}
161 public void refreshSystemFavorites() {
162 lastUpdateLock.lock();
164 final long now = System.currentTimeMillis();
165 if (now > lastUpdateTime + UPDATE_TIME_SPAN) {
166 lastUpdateTime = now;
167 for (int x = 1; x <= 32; x++) {
168 sendCommand("GET System.favorite[" + x + "].valid");
169 sendCommand("GET System.favorite[" + x + "].name");
173 lastUpdateLock.unlock();
178 * Returns the JSON representation of all the system favorites and their state.
180 * @return A non-null, non-empty JSON representation of {@link #systemFavorites}
182 public String getJson() {
183 final List<RioFavorite> favs = new ArrayList<>();
184 for (final RioFavorite fav : systemFavorites) {
189 return gson.toJson(favs);
193 * Sets the system favorites for a controller/zone. For each system favorite found in the favJson parameter, this
194 * method will either save the system favorite (if it's status changed from not valid to valid) or save the system
195 * favorite name (if only the name changed) or delete the system favorite (if status changed from valid to invalid).
197 * @param controller the controller number between 1 and 6
198 * @param zone the zone number between 1 and 8
199 * @param favJson the possibly empty, possibly null JSON representation of system favorites
200 * @throws IllegalArgumentException if controller is < 1 or > 6
201 * @throws IllegalArgumentException if zone is < 1 or > 8
203 public void setSystemFavorites(int controller, int zone, @Nullable String favJson) {
204 if (controller < 1 || controller > 6) {
205 throw new IllegalArgumentException("Controller must be between 1 and 6");
208 if (zone < 1 || zone > 8) {
209 throw new IllegalArgumentException("Zone must be between 1 and 8");
212 if (favJson == null || favJson.isEmpty()) {
216 final List<Integer> updateFavIds = new ArrayList<>();
218 final RioFavorite[] favs;
219 favs = gson.fromJson(favJson, RioFavorite[].class);
220 for (int x = favs.length - 1; x >= 0; x--) {
221 final RioFavorite fav = favs[x];
223 continue; // caused by {id,valid,name},,{id,valid,name}
226 final int favId = fav.getId();
227 if (favId < 1 || favId > 32) {
228 logger.debug("Invalid favorite id (not between 1 and 32) - ignoring: {}:{}", favId, favJson);
230 final RioFavorite myFav = systemFavorites[favId - 1];
231 final boolean favValid = fav.isValid();
232 final String favName = fav.getName();
234 // re-retrieve to see if the save/delete worked (saving on a zone that's off - valid won't be set to
236 if (myFav.isValid() != favValid) {
237 myFav.setValid(favValid);
239 myFav.setName(favName);
240 sendCommand("EVENT C[" + controller + "].Z[" + zone + "]!saveSystemFavorite \"" + favName
242 updateFavIds.add(favId);
244 sendCommand("EVENT C[" + controller + "].Z[" + zone + "]!deleteSystemFavorite " + favId);
246 } else if (!Objects.equals(myFav.getName(), favName)) {
247 myFav.setName(favName);
248 sendCommand("SET System.favorite[" + favId + "]." + FAV_NAME + "=\"" + favName + "\"");
252 } catch (JsonSyntaxException e) {
253 logger.debug("Invalid JSON: {}", e.getMessage(), e);
256 // Refresh the favorites that changed (verifies if the favorite was actually saved)
257 requestSystemFavorites(updateFavIds);
259 // Refresh any listeners immediately to reset the channel
264 * Handles any system notifications returned by the russound system
266 * @param m a non-null matcher
267 * @param resp a possibly null, possibly empty response
269 private void handleSystemFavoriteNotification(Matcher m, String resp) {
271 throw new IllegalArgumentException("m (matcher) cannot be null");
273 if (m.groupCount() == 3) {
275 final int favoriteId = Integer.parseInt(m.group(1));
277 if (favoriteId >= 1 && favoriteId <= 32) {
278 final RioFavorite fav = systemFavorites[favoriteId - 1];
280 final String key = m.group(2).toLowerCase();
281 final String value = m.group(3);
289 fav.setValid(!"false".equalsIgnoreCase(value));
294 logger.warn("Unknown system favorite notification: '{}'", resp);
298 logger.warn("Invalid System Favorite Notification (favorite < 1 or > 32): '{}')", resp);
300 } catch (NumberFormatException e) {
301 logger.warn("Invalid System Favorite Notification (favorite not a parsable integer): '{}')", resp);
304 logger.warn("Invalid System Notification response: '{}'", resp);
309 * Implements {@link SocketSessionListener#responseReceived(String)} to try to process the response from the
310 * russound system. This response may be for other protocol handler - so ignore if we don't recognize the response.
312 * @param a possibly null, possibly empty response
315 public void responseReceived(@Nullable String response) {
316 if (response == null || response.isEmpty()) {
320 final Matcher m = RSP_SYSTEMFAVORITENOTIFICATION.matcher(response);
322 handleSystemFavoriteNotification(m, response);
327 * Defines the listener implementation to list for system favorite updates
329 * @author Tim Roberts
332 public interface Listener {
334 * Called when system favorites have changed. The jsonString will contain the current representation of all
335 * valid system favorites.
337 * @param jsonString a non-null, non-empty json representation of {@link RioFavorite}
339 void systemFavoritesUpdated(String jsonString);