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