]> git.basschouten.com Git - openhab-addons.git/blob
60a7bc7ba20b26065495410aea21aba092625919
[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.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.Map;
20 import java.util.Map.Entry;
21 import java.util.Optional;
22 import java.util.Set;
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 increaseDecreaseCommand) {
171                         systemControl.setPartyModeVolume(increaseDecreaseCommand == IncreaseDecreaseType.INCREASE);
172                     } else {
173                         logger.warn("Only {} and {} commands are supported for {}", IncreaseDecreaseType.DECREASE,
174                                 IncreaseDecreaseType.DECREASE, id);
175                     }
176                     break;
177                 default:
178                     logger.warn(
179                             "Channel {} not supported on the yamaha device directly! Try with the zone things instead.",
180                             id);
181             }
182         } catch (IOException | ReceivedMessageParseException e) {
183             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
184         }
185     }
186
187     private void refreshFromState(ChannelUID channelUID) {
188         // Might be extended in the future, therefore a switch statement
189         switch (channelUID.getId()) {
190             case CHANNEL_POWER:
191                 updateState(channelUID, OnOffType.from(systemControlState.power));
192                 break;
193             case CHANNEL_PARTY_MODE:
194                 updateState(channelUID, OnOffType.from(systemControlState.partyMode));
195                 break;
196             case CHANNEL_PARTY_MODE_MUTE:
197             case CHANNEL_PARTY_MODE_VOLUME:
198                 // no state updates available
199                 break;
200             default:
201                 logger.warn("Channel refresh for {} not implemented!", channelUID.getId());
202         }
203     }
204
205     /**
206      * Sets up a refresh timer (using the scheduler) with the given interval.
207      *
208      * @param initialWaitTime The delay before the first refresh. Maybe 0 to immediately
209      *            initiate a refresh.
210      */
211     private void setupRefreshTimer(int initialWaitTime) {
212         cancelRefreshTimer();
213         logger.trace("Setting up refresh timer with fixed delay {} seconds, starting in {} seconds",
214                 bridgeConfig.getRefreshInterval(), initialWaitTime);
215         refreshTimer = scheduler.scheduleWithFixedDelay(() -> updateAllZoneInformation(), initialWaitTime,
216                 bridgeConfig.getRefreshInterval(), TimeUnit.SECONDS);
217     }
218
219     /**
220      * Cancels the refresh timer (if one was setup).
221      */
222     private void cancelRefreshTimer() {
223         if (refreshTimer != null) {
224             refreshTimer.cancel(false);
225             refreshTimer = null;
226         }
227     }
228
229     /**
230      * Periodically and initially called. This must run in another thread, because all update calls are blocking.
231      */
232     void updateAllZoneInformation() {
233         if (disposed) {
234             logger.trace("updateAllZoneInformation will be skipped because the bridge is disposed");
235             return;
236         }
237
238         if (!ensureConnectionInitialized()) {
239             // The initialization did not yet happen and the device is still offline (or not reachable)
240             return;
241         }
242
243         logger.trace("updateAllZoneInformation");
244         try {
245             // Set power = true before calling systemControl.update(),
246             // otherwise the systemControlStateChanged method would call updateAllZoneInformation() again
247             systemControlState.power = true;
248             systemControl.update();
249
250             updateStatus(ThingStatus.ONLINE);
251
252             Bridge bridge = (Bridge) thing;
253             for (Thing thing : bridge.getThings()) {
254                 YamahaZoneThingHandler handler = (YamahaZoneThingHandler) thing.getHandler();
255
256                 // Ensure the handler has been already assigned
257                 if (handler != null && handler.isCorrectlyInitialized()) {
258                     handler.updateZoneInformation();
259                 }
260             }
261         } catch (IOException e) {
262             systemControlState.invalidate();
263             onConnectivityError(e);
264             return;
265         } catch (ReceivedMessageParseException e) {
266             String message = e.getMessage();
267             updateProperty(PROPERTY_MENU_ERROR, message != null ? message : "");
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 Set.of(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.isEmpty()) {
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, OnOffType.from(systemControlState.power));
416         if (needsCompleteRefresh) {
417             updateAllZoneInformation();
418         }
419     }
420 }