]> git.basschouten.com Git - openhab-addons.git/blob
dbf7f2e0680379284d0ddfa70ce0c7b5d1c9e293
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.russound.internal.rio;
14
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;
23
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;
31
32 import com.google.gson.Gson;
33 import com.google.gson.JsonSyntaxException;
34
35 /**
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}.
39  *
40  * @author Tim Roberts - Initial contribution
41  */
42 public class RioSystemFavoritesProtocol extends AbstractRioProtocol {
43
44     // logger
45     private final Logger logger = LoggerFactory.getLogger(RioSystemFavoritesProtocol.class);
46
47     // Helper names in the protocol
48     private static final String FAV_NAME = "name";
49     private static final String FAV_VALID = "valid";
50
51     /**
52      * The pattern representing system favorite notifications
53      */
54     private static final Pattern RSP_SYSTEMFAVORITENOTIFICATION = Pattern
55             .compile("(?i)^[SN] System.favorite\\[(\\d+)\\].(\\w+)=\"(.*)\"$");
56
57     /**
58      * The current state of all 32 system favorites
59      */
60     private final RioFavorite[] systemFavorites = new RioFavorite[32];
61
62     /**
63      * The {@link Gson} used for all JSON operations
64      */
65     private final Gson gson;
66
67     /**
68      * The {@link ReentrantLock} used to control access to {@link #lastUpdateTime}
69      */
70     private final Lock lastUpdateLock = new ReentrantLock();
71
72     /**
73      * The last time we did a full refresh of system favorites via {@link #refreshSystemFavorites()}
74      */
75     private long lastUpdateTime;
76
77     /**
78      * The minimum timespan between full refreshes of system favorites (via {@link #refreshSystemFavorites()})
79      */
80     private static final long UPDATE_TIME_SPAN = 60000;
81
82     /**
83      * The list of listeners that will be called when system favorites have changed
84      */
85     private final CopyOnWriteArrayList<Listener> listeners = new CopyOnWriteArrayList<>();
86
87     /**
88      * Constructs the system favorite protocol from the given session and callback. Note: the passed callback is not
89      * currently used
90      *
91      * @param session a non null {@link SocketSession} to use
92      * @param callback a non-null {@link RioHandlerCallback} to use
93      */
94     public RioSystemFavoritesProtocol(SocketSession session, RioHandlerCallback callback) {
95         super(session, callback);
96
97         gson = GsonUtilities.createGson();
98
99         for (int x = 1; x <= 32; x++) {
100             systemFavorites[x - 1] = new RioFavorite(x);
101         }
102     }
103
104     /**
105      * Adds the specified listener to changes in system favorites
106      *
107      * @param listener a non-null listener to add
108      * @throws IllegalArgumentException if listener is null
109      */
110     public void addListener(Listener listener) {
111         if (listener == null) {
112             throw new IllegalArgumentException("listener cannot be null");
113         }
114         listeners.add(listener);
115     }
116
117     /**
118      * Removes the specified listener from change notifications
119      *
120      * @param listener a possibly null listener to remove (null is ignored)
121      * @return true if removed, false otherwise
122      */
123     public boolean removeListener(Listener listener) {
124         return listeners.remove(listener);
125     }
126
127     /**
128      * Fires the systemFavoritesUpdated method on all listeners with the results of {@link #getJson()}
129      */
130     private void fireUpdate() {
131         final String json = getJson();
132         for (Listener l : listeners) {
133             l.systemFavoritesUpdated(json);
134         }
135     }
136
137     /**
138      * Helper method to request the specified system favorite id information (name/valid). Please note that this does
139      * NOT change the {@link #lastUpdateTime}
140      *
141      * @param favIds a non-null, possibly empty list of system favorite ids to request (any id < 1 or > 32 will be
142      *            ignored)
143      * @throws IllegalArgumentException if favIds is null
144      */
145     private void requestSystemFavorites(List<Integer> favIds) {
146         if (favIds == null) {
147             throw new IllegalArgumentException("favIds cannot be null");
148         }
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");
153             }
154         }
155     }
156
157     /**
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}
160      */
161     public void refreshSystemFavorites() {
162         lastUpdateLock.lock();
163         try {
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");
170                 }
171             }
172         } finally {
173             lastUpdateLock.unlock();
174         }
175     }
176
177     /**
178      * Returns the JSON representation of all the system favorites and their state.
179      *
180      * @return A non-null, non-empty JSON representation of {@link #systemFavorites}
181      */
182     public String getJson() {
183         final List<RioFavorite> favs = new ArrayList<>();
184         for (final RioFavorite fav : systemFavorites) {
185             if (fav.isValid()) {
186                 favs.add(fav);
187             }
188         }
189         return gson.toJson(favs);
190     }
191
192     /**
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).
196      *
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 {@literal <} 1 or > 6
201      * @throws IllegalArgumentException if zone is {@literal < 1} or > 8
202      */
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");
206         }
207
208         if (zone < 1 || zone > 8) {
209             throw new IllegalArgumentException("Zone must be between 1 and 8");
210         }
211
212         if (favJson == null || favJson.isEmpty()) {
213             return;
214         }
215
216         final List<Integer> updateFavIds = new ArrayList<>();
217         try {
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];
222                 if (fav == null) {
223                     continue; // caused by {id,valid,name},,{id,valid,name}
224                 }
225
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);
229                 } else {
230                     final RioFavorite myFav = systemFavorites[favId - 1];
231                     final boolean favValid = fav.isValid();
232                     final String favName = fav.getName();
233
234                     // re-retrieve to see if the save/delete worked (saving on a zone that's off - valid won't be set to
235                     // true)
236                     if (myFav.isValid() != favValid) {
237                         myFav.setValid(favValid);
238                         if (favValid) {
239                             myFav.setName(favName);
240                             sendCommand("EVENT C[" + controller + "].Z[" + zone + "]!saveSystemFavorite \"" + favName
241                                     + "\" " + favId);
242                             updateFavIds.add(favId);
243                         } else {
244                             sendCommand("EVENT C[" + controller + "].Z[" + zone + "]!deleteSystemFavorite " + favId);
245                         }
246                     } else if (!Objects.equals(myFav.getName(), favName)) {
247                         myFav.setName(favName);
248                         sendCommand("SET System.favorite[" + favId + "]." + FAV_NAME + "=\"" + favName + "\"");
249                     }
250                 }
251             }
252         } catch (JsonSyntaxException e) {
253             logger.debug("Invalid JSON: {}", e.getMessage(), e);
254         }
255
256         // Refresh the favorites that changed (verifies if the favorite was actually saved)
257         requestSystemFavorites(updateFavIds);
258
259         // Refresh any listeners immediately to reset the channel
260         fireUpdate();
261     }
262
263     /**
264      * Handles any system notifications returned by the russound system
265      *
266      * @param m a non-null matcher
267      * @param resp a possibly null, possibly empty response
268      */
269     private void handleSystemFavoriteNotification(Matcher m, String resp) {
270         if (m == null) {
271             throw new IllegalArgumentException("m (matcher) cannot be null");
272         }
273         if (m.groupCount() == 3) {
274             try {
275                 final int favoriteId = Integer.parseInt(m.group(1));
276
277                 if (favoriteId >= 1 && favoriteId <= 32) {
278                     final RioFavorite fav = systemFavorites[favoriteId - 1];
279
280                     final String key = m.group(2).toLowerCase();
281                     final String value = m.group(3);
282
283                     switch (key) {
284                         case FAV_NAME:
285                             fav.setName(value);
286                             fireUpdate();
287                             break;
288                         case FAV_VALID:
289                             fav.setValid(!"false".equalsIgnoreCase(value));
290                             fireUpdate();
291                             break;
292
293                         default:
294                             logger.warn("Unknown system favorite notification: '{}'", resp);
295                             break;
296                     }
297                 } else {
298                     logger.warn("Invalid System Favorite Notification (favorite < 1 or > 32): '{}')", resp);
299                 }
300             } catch (NumberFormatException e) {
301                 logger.warn("Invalid System Favorite Notification (favorite not a parsable integer): '{}')", resp);
302             }
303         } else {
304             logger.warn("Invalid System Notification response: '{}'", resp);
305         }
306     }
307
308     /**
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.
311      *
312      * @param response a possibly null, possibly empty response
313      */
314     @Override
315     public void responseReceived(@Nullable String response) {
316         if (response == null || response.isEmpty()) {
317             return;
318         }
319
320         final Matcher m = RSP_SYSTEMFAVORITENOTIFICATION.matcher(response);
321         if (m.matches()) {
322             handleSystemFavoriteNotification(m, response);
323         }
324     }
325
326     /**
327      * Defines the listener implementation to list for system favorite updates
328      *
329      * @author Tim Roberts
330      *
331      */
332     public interface Listener {
333         /**
334          * Called when system favorites have changed. The jsonString will contain the current representation of all
335          * valid system favorites.
336          *
337          * @param jsonString a non-null, non-empty json representation of {@link RioFavorite}
338          */
339         void systemFavoritesUpdated(String jsonString);
340     }
341 }