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.system;
15 import java.util.concurrent.atomic.AtomicBoolean;
16 import java.util.regex.Matcher;
17 import java.util.regex.Pattern;
19 import org.eclipse.jdt.annotation.Nullable;
20 import org.openhab.binding.russound.internal.net.SocketSession;
21 import org.openhab.binding.russound.internal.net.SocketSessionListener;
22 import org.openhab.binding.russound.internal.rio.AbstractRioProtocol;
23 import org.openhab.binding.russound.internal.rio.RioConstants;
24 import org.openhab.binding.russound.internal.rio.RioHandlerCallback;
25 import org.openhab.core.library.types.OnOffType;
26 import org.openhab.core.library.types.StringType;
27 import org.openhab.core.thing.ThingStatus;
28 import org.openhab.core.thing.ThingStatusDetail;
29 import org.slf4j.Logger;
30 import org.slf4j.LoggerFactory;
33 * This is the protocol handler for the Russound System. This handler will issue the protocol commands and will
34 * process the responses from the Russound system.
36 * @author Tim Roberts - Initial contribution
38 class RioSystemProtocol extends AbstractRioProtocol {
40 private final Logger logger = LoggerFactory.getLogger(RioSystemProtocol.class);
43 private static final String SYS_VERSION = "version"; // 12 max
44 private static final String SYS_STATUS = "status"; // 12 max
45 private static final String SYS_LANG = "language"; // 12 max
48 private static final Pattern RSP_VERSION = Pattern.compile("(?i)^S VERSION=\"(.+)\"$");
49 private static final Pattern RSP_FAILURE = Pattern.compile("(?i)^E (.*)");
50 private static final Pattern RSP_SYSTEMNOTIFICATION = Pattern.compile("(?i)^[SN] System\\.(\\w+)=\"(.*)\"$");
52 // all on state (there is no corresponding value)
53 private final AtomicBoolean allOn = new AtomicBoolean(false);
56 * This represents our ping command. There is no ping command in the protocol so we simply send an empty command to
57 * keep things alive (and not generate any errors)
59 private static final String CMD_PING = "";
62 * Constructs the protocol handler from given parameters
64 * @param session a non-null {@link SocketSession} (may be connected or disconnected)
65 * @param callback a non-null {@link RioHandlerCallback} to callback
67 RioSystemProtocol(SocketSession session, RioHandlerCallback callback) {
68 super(session, callback);
72 * Attempts to log into the system. The russound system requires no login, so we immediately execute any
73 * {@link #postLogin()} commands.
75 * @return always null to indicate a successful login
83 * Post successful login stuff - mark us online, start watching the system and refresh some attributes
85 private void postLogin() {
86 logger.info("Russound System now connected");
87 statusChanged(ThingStatus.ONLINE, ThingStatusDetail.NONE, null);
90 refreshSystemStatus();
92 refreshSystemLanguage();
96 * Pings the server with out ping command to keep the connection alive
99 sendCommand(CMD_PING);
103 * Refreshes the firmware version of the system
105 void refreshVersion() {
106 sendCommand(SYS_VERSION);
110 * Helper method to refresh a system keyname
112 * @param keyName a non-null, non-empty keyname
113 * @throws IllegalArgumentException if keyname is null or empty
115 private void refreshSystemKey(String keyName) {
116 if (keyName == null || keyName.trim().length() == 0) {
117 throw new IllegalArgumentException("keyName cannot be null or empty");
120 sendCommand("GET System." + keyName);
124 * Refresh the system status
126 void refreshSystemAllOn() {
127 stateChanged(RioConstants.CHANNEL_SYSALLON, OnOffType.from(allOn.get()));
131 * Refresh the system language
133 void refreshSystemLanguage() {
134 refreshSystemKey(SYS_LANG);
138 * Refresh the system status
140 void refreshSystemStatus() {
141 refreshSystemKey(SYS_STATUS);
145 * Turns on/off watching for system notifications
147 * @param on true to turn on, false to turn off
149 void watchSystem(boolean on) {
150 sendCommand("WATCH SYSTEM " + (on ? "ON" : "OFF"));
156 * @param on true to turn all zones on, false otherwise
158 void setSystemAllOn(boolean on) {
159 sendCommand("EVENT C[1].Z[1]!All" + (on ? "On" : "Off"));
161 refreshSystemAllOn();
165 * Sets the system language (currently can only be english, chinese or russian). Case does not matter - will be
166 * converted to uppercase for the system.
168 * @param language a non-null, non-empty language to set
169 * @throws IllegalArgumentException if language is null, empty or not (english, chinese or russian).
171 void setSystemLanguage(String language) {
172 if (language == null || language.trim().length() == 0) {
173 throw new IllegalArgumentException("Language cannot be null or an empty string");
176 if ("|ENGLISH|CHINESE|RUSSIAN|".indexOf("|" + language + "|") == -1) {
177 throw new IllegalArgumentException("Language can only be ENGLISH, CHINESE or RUSSIAN: " + language);
179 sendCommand("SET System." + SYS_LANG + " " + language.toUpperCase());
183 * Handles the version notification
185 * @param m a non-null matcher
186 * @param resp a possibly null, possibly empty response
188 void handleVersionNotification(Matcher m, String resp) {
190 throw new IllegalArgumentException("m (matcher) cannot be null");
192 if (m.groupCount() == 1) {
193 final String version = m.group(1);
194 setProperty(RioConstants.PROPERTY_SYSVERSION, version);
196 logger.warn("Invalid System Notification response: '{}'", resp);
201 * Handles any system notifications returned by the russound system
203 * @param m a non-null matcher
204 * @param resp a possibly null, possibly empty response
206 void handleSystemNotification(Matcher m, String resp) {
208 throw new IllegalArgumentException("m (matcher) cannot be null");
210 if (m.groupCount() == 2) {
211 final String key = m.group(1).toLowerCase();
212 final String value = m.group(2);
216 stateChanged(RioConstants.CHANNEL_SYSLANG, new StringType(value));
219 stateChanged(RioConstants.CHANNEL_SYSSTATUS, OnOffType.from("ON".equals(value)));
223 logger.warn("Unknown system notification: '{}'", resp);
227 logger.warn("Invalid System Notification response: '{}'", resp);
232 * Handles any error notifications returned by the russound system
234 * @param m a non-null matcher
235 * @param resp a possibly null, possibly empty response
237 private void handleFailureNotification(Matcher m, String resp) {
238 logger.debug("Error notification: {}", resp);
242 * Implements {@link SocketSessionListener#responseReceived(String)} to try to process the response from the
243 * russound system. This response may be for other protocol handler - so ignore if we don't recognize the response.
245 * @param a possibly null, possibly empty response
248 public void responseReceived(@Nullable String response) {
249 if (response == null || response.isEmpty()) {
253 Matcher m = RSP_VERSION.matcher(response);
255 handleVersionNotification(m, response);
259 m = RSP_SYSTEMNOTIFICATION.matcher(response);
261 handleSystemNotification(m, response);
265 m = RSP_FAILURE.matcher(response);
267 handleFailureNotification(m, response);
273 * Overrides the default implementation to turn watch off ({@link #watchSystem(boolean)}) before calling the dispose
276 public void dispose() {