]> git.basschouten.com Git - openhab-addons.git/blob
42725d7fe819dfcea035c42be38750b50b3ce611
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 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.yamahareceiver.internal.handler;
14
15 import static org.openhab.binding.yamahareceiver.internal.YamahaReceiverBindingConstants.*;
16
17 import java.io.IOException;
18 import java.util.Collection;
19 import java.util.Collections;
20 import java.util.Map;
21 import java.util.Map.Entry;
22 import java.util.Optional;
23 import java.util.concurrent.CountDownLatch;
24 import java.util.concurrent.ScheduledFuture;
25 import java.util.concurrent.TimeUnit;
26
27 import org.openhab.binding.yamahareceiver.internal.config.YamahaBridgeConfig;
28 import org.openhab.binding.yamahareceiver.internal.discovery.ZoneDiscoveryService;
29 import org.openhab.binding.yamahareceiver.internal.protocol.AbstractConnection;
30 import org.openhab.binding.yamahareceiver.internal.protocol.ConnectionStateListener;
31 import org.openhab.binding.yamahareceiver.internal.protocol.DeviceInformation;
32 import org.openhab.binding.yamahareceiver.internal.protocol.InputConverter;
33 import org.openhab.binding.yamahareceiver.internal.protocol.ProtocolFactory;
34 import org.openhab.binding.yamahareceiver.internal.protocol.ReceivedMessageParseException;
35 import org.openhab.binding.yamahareceiver.internal.protocol.SystemControl;
36 import org.openhab.binding.yamahareceiver.internal.protocol.xml.XMLProtocolFactory;
37 import org.openhab.binding.yamahareceiver.internal.state.DeviceInformationState;
38 import org.openhab.binding.yamahareceiver.internal.state.SystemControlState;
39 import org.openhab.binding.yamahareceiver.internal.state.SystemControlStateListener;
40 import org.openhab.core.config.core.Configuration;
41 import org.openhab.core.library.types.IncreaseDecreaseType;
42 import org.openhab.core.library.types.OnOffType;
43 import org.openhab.core.thing.Bridge;
44 import org.openhab.core.thing.ChannelUID;
45 import org.openhab.core.thing.Thing;
46 import org.openhab.core.thing.ThingStatus;
47 import org.openhab.core.thing.ThingStatusDetail;
48 import org.openhab.core.thing.binding.BaseBridgeHandler;
49 import org.openhab.core.thing.binding.ThingHandlerService;
50 import org.openhab.core.types.Command;
51 import org.openhab.core.types.RefreshType;
52 import org.slf4j.Logger;
53 import org.slf4j.LoggerFactory;
54
55 /**
56  * The {@link YamahaBridgeHandler} is responsible for fetching basic information about the
57  * found AVR and start the zone detection.
58  *
59  * @author David Graeff - Initial contribution
60  * @author Tomasz Maruszak - Input mapping fix, volumeDB fix, better feature detection, added config object
61  */
62 public class YamahaBridgeHandler extends BaseBridgeHandler
63         implements ConnectionStateListener, SystemControlStateListener {
64
65     private final Logger logger = LoggerFactory.getLogger(YamahaBridgeHandler.class);
66
67     private YamahaBridgeConfig bridgeConfig;
68     private InputConverter inputConverter;
69
70     private ScheduledFuture<?> refreshTimer;
71     private ZoneDiscoveryService zoneDiscoveryService;
72
73     private final DeviceInformationState deviceInformationState = new DeviceInformationState();
74
75     private SystemControl systemControl;
76     private SystemControlState systemControlState = new SystemControlState();
77
78     private final CountDownLatch loadingDone = new CountDownLatch(1);
79
80     private boolean disposed = false;
81
82     private ProtocolFactory protocolFactory;
83     private AbstractConnection connection;
84
85     public YamahaBridgeHandler(Bridge bridge) {
86         super(bridge);
87         protocolFactory = new XMLProtocolFactory();
88     }
89
90     /**
91      * Return the input mapping converter
92      */
93     public InputConverter getInputConverter() {
94         return inputConverter;
95     }
96
97     /**
98      * @return Return the protocol communication object. This may be null
99      *         if the bridge is offline.
100      */
101     public AbstractConnection getConnection() {
102         return connection;
103     }
104
105     /**
106      * Gets the current protocol factory.
107      *
108      * @return
109      */
110     public ProtocolFactory getProtocolFactory() {
111         return protocolFactory;
112     }
113
114     /**
115      * Sets the current protocol factory.
116      *
117      * @param protocolFactory
118      */
119     public void setProtocolFactory(ProtocolFactory protocolFactory) {
120         this.protocolFactory = protocolFactory;
121     }
122
123     @Override
124     public void dispose() {
125         cancelRefreshTimer();
126
127         super.dispose();
128         disposed = true;
129     }
130
131     /**
132      * Returns the device information
133      */
134     public DeviceInformationState getDeviceInformationState() {
135         return deviceInformationState;
136     }
137
138     /**
139      * Returns the device configuration
140      */
141     public YamahaBridgeConfig getConfiguration() {
142         return bridgeConfig;
143     }
144
145     @Override
146     public void handleCommand(ChannelUID channelUID, Command command) {
147         if (connection == null || deviceInformationState.host == null) {
148             return;
149         }
150
151         if (command instanceof RefreshType) {
152             refreshFromState(channelUID);
153             return;
154         }
155
156         try {
157             // Might be extended in the future, therefore a switch statement
158             String id = channelUID.getId();
159             switch (id) {
160                 case CHANNEL_POWER:
161                     systemControl.setPower(((OnOffType) command) == OnOffType.ON);
162                     break;
163                 case CHANNEL_PARTY_MODE:
164                     systemControl.setPartyMode(((OnOffType) command) == OnOffType.ON);
165                     break;
166                 case CHANNEL_PARTY_MODE_MUTE:
167                     systemControl.setPartyModeMute(((OnOffType) command) == OnOffType.ON);
168                     break;
169                 case CHANNEL_PARTY_MODE_VOLUME:
170                     if (command instanceof IncreaseDecreaseType) {
171                         systemControl
172                                 .setPartyModeVolume(((IncreaseDecreaseType) command) == IncreaseDecreaseType.INCREASE);
173                     } else {
174                         logger.warn("Only {} and {} commands are supported for {}", IncreaseDecreaseType.DECREASE,
175                                 IncreaseDecreaseType.DECREASE, id);
176                     }
177                     break;
178                 default:
179                     logger.warn(
180                             "Channel {} not supported on the yamaha device directly! Try with the zone things instead.",
181                             id);
182             }
183         } catch (IOException | ReceivedMessageParseException e) {
184             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
185         }
186     }
187
188     private void refreshFromState(ChannelUID channelUID) {
189         // Might be extended in the future, therefore a switch statement
190         switch (channelUID.getId()) {
191             case CHANNEL_POWER:
192                 updateState(channelUID, systemControlState.power ? OnOffType.ON : OnOffType.OFF);
193                 break;
194             case CHANNEL_PARTY_MODE:
195                 updateState(channelUID, systemControlState.partyMode ? OnOffType.ON : OnOffType.OFF);
196                 break;
197             case CHANNEL_PARTY_MODE_MUTE:
198             case CHANNEL_PARTY_MODE_VOLUME:
199                 // no state updates available
200                 break;
201             default:
202                 logger.warn("Channel refresh for {} not implemented!", channelUID.getId());
203         }
204     }
205
206     /**
207      * Sets up a refresh timer (using the scheduler) with the given interval.
208      *
209      * @param initialWaitTime The delay before the first refresh. Maybe 0 to immediately
210      *            initiate a refresh.
211      */
212     private void setupRefreshTimer(int initialWaitTime) {
213         cancelRefreshTimer();
214         logger.trace("Setting up refresh timer with fixed delay {} seconds, starting in {} seconds",
215                 bridgeConfig.getRefreshInterval(), initialWaitTime);
216         refreshTimer = scheduler.scheduleWithFixedDelay(() -> updateAllZoneInformation(), initialWaitTime,
217                 bridgeConfig.getRefreshInterval(), TimeUnit.SECONDS);
218     }
219
220     /**
221      * Cancels the refresh timer (if one was setup).
222      */
223     private void cancelRefreshTimer() {
224         if (refreshTimer != null) {
225             refreshTimer.cancel(false);
226             refreshTimer = null;
227         }
228     }
229
230     /**
231      * Periodically and initially called. This must run in another thread, because all update calls are blocking.
232      */
233     void updateAllZoneInformation() {
234         if (disposed) {
235             logger.trace("updateAllZoneInformation will be skipped because the bridge is disposed");
236             return;
237         }
238
239         if (!ensureConnectionInitialized()) {
240             // The initialization did not yet happen and the device is still offline (or not reachable)
241             return;
242         }
243
244         logger.trace("updateAllZoneInformation");
245         try {
246             // Set power = true before calling systemControl.update(),
247             // otherwise the systemControlStateChanged method would call updateAllZoneInformation() again
248             systemControlState.power = true;
249             systemControl.update();
250
251             updateStatus(ThingStatus.ONLINE);
252
253             Bridge bridge = (Bridge) thing;
254             for (Thing thing : bridge.getThings()) {
255                 YamahaZoneThingHandler handler = (YamahaZoneThingHandler) thing.getHandler();
256
257                 // Ensure the handler has been already assigned
258                 if (handler != null && handler.isCorrectlyInitialized()) {
259                     handler.updateZoneInformation();
260                 }
261             }
262         } catch (IOException e) {
263             systemControlState.invalidate();
264             onConnectivityError(e);
265             return;
266         } catch (ReceivedMessageParseException e) {
267             updateProperty(PROPERTY_MENU_ERROR, e.getMessage());
268             // Some AVRs send unexpected responses. We log parser exceptions therefore.
269             logger.debug("Parse error!", e);
270         } finally {
271             loadingDone.countDown();
272         }
273     }
274
275     /**
276      * We handle the update ourselves to avoid a costly dispose/initialize
277      */
278     @Override
279     public void handleConfigurationUpdate(Map<String, Object> configurationParameters) {
280         if (!isInitialized()) {
281             super.handleConfigurationUpdate(configurationParameters);
282             return;
283         }
284
285         validateConfigurationParameters(configurationParameters);
286
287         // can be overridden by subclasses
288         Configuration configurationObject = editConfiguration();
289         for (Entry<String, Object> configurationParameter : configurationParameters.entrySet()) {
290             configurationObject.put(configurationParameter.getKey(), configurationParameter.getValue());
291         }
292
293         updateConfiguration(configurationObject);
294
295         bridgeConfig = configurationObject.as(YamahaBridgeConfig.class);
296         logger.trace("Update configuration of {} with host '{}' and port {}", getThing().getLabel(),
297                 bridgeConfig.getHost(), bridgeConfig.getPort());
298
299         Optional<String> host = bridgeConfig.getHostWithPort();
300         if (host.isPresent()) {
301             connection.setHost(host.get());
302             onConnectionCreated(connection);
303         }
304
305         inputConverter = protocolFactory.InputConverter(connection, bridgeConfig.getInputMapping());
306         setupRefreshTimer(bridgeConfig.getRefreshInterval());
307     }
308
309     /**
310      * We handle updates of this thing ourself.
311      */
312     @Override
313     public void thingUpdated(Thing thing) {
314         this.thing = thing;
315     }
316
317     @Override
318     public Collection<Class<? extends ThingHandlerService>> getServices() {
319         return Collections.singleton(ZoneDiscoveryService.class);
320     }
321
322     /**
323      * Called by the zone discovery service to let this handler have a reference.
324      */
325     public void setZoneDiscoveryService(ZoneDiscoveryService s) {
326         this.zoneDiscoveryService = s;
327     }
328
329     /**
330      * Calls createCommunicationObject if the host name is configured correctly.
331      */
332     @Override
333     public void initialize() {
334         bridgeConfig = getConfigAs(YamahaBridgeConfig.class);
335         logger.trace("Initialize of {} with host '{}' and port {}", getThing().getLabel(), bridgeConfig.getHost(),
336                 bridgeConfig.getPort());
337
338         Optional<String> host = bridgeConfig.getHostWithPort();
339         if (!host.isPresent()) {
340             String msg = "Host or port not set. Double check your thing settings.";
341             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, msg);
342             logger.warn(msg);
343             return;
344         }
345
346         if (zoneDiscoveryService == null) {
347             logger.warn("Zone discovery service not ready!");
348             return;
349         }
350
351         protocolFactory.createConnection(host.get(), this);
352     }
353
354     @Override
355     public void onConnectionCreated(AbstractConnection connection) {
356         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
357                 "Waiting for connection with Yamaha device");
358
359         this.connection = connection;
360         this.systemControl = null;
361
362         if (!ensureConnectionInitialized()) {
363             logger.warn("Communication error. Please review your Yamaha thing configuration.");
364         }
365
366         setupRefreshTimer(0);
367     }
368
369     /**
370      * Attempts to perform a one-time initialization after a connection is created.
371      *
372      * @return true if initialization was successful
373      */
374     private boolean ensureConnectionInitialized() {
375         if (systemControl != null) {
376             return true;
377         }
378
379         logger.trace("Initializing connection");
380
381         try {
382             DeviceInformation deviceInformation = protocolFactory.DeviceInformation(connection, deviceInformationState);
383             deviceInformation.update();
384
385             updateProperty(PROPERTY_VERSION, deviceInformationState.version);
386             updateProperty(PROPERTY_ASSIGNED_NAME, deviceInformationState.name);
387
388             zoneDiscoveryService.publishZones(deviceInformationState, thing.getUID());
389
390             systemControl = protocolFactory.SystemControl(connection, this, deviceInformationState);
391             inputConverter = protocolFactory.InputConverter(connection, bridgeConfig.getInputMapping());
392         } catch (IOException | ReceivedMessageParseException e) {
393             deviceInformationState.invalidate();
394             onConnectivityError(e);
395             return false;
396         }
397         return true;
398     }
399
400     private void onConnectivityError(Exception e) {
401         String description = e.getMessage();
402         logger.debug(
403                 "Communication error. Either the Yamaha thing configuration is invalid or the device is offline. Details: {}",
404                 description);
405         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, description);
406     }
407
408     @Override
409     public void systemControlStateChanged(SystemControlState msg) {
410         // If the device was off and now turns on, we trigger a refresh of all zone things.
411         // The user might have renamed some of the inputs etc.
412         boolean needsCompleteRefresh = msg.power && !systemControlState.power;
413         systemControlState = msg;
414
415         updateState(CHANNEL_POWER, systemControlState.power ? OnOffType.ON : OnOffType.OFF);
416         if (needsCompleteRefresh) {
417             updateAllZoneInformation();
418         }
419     }
420 }