]> git.basschouten.com Git - openhab-addons.git/blob
d450c17a51ca62ff4b7ca01eb3598902fa64c001
[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.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             String message = e.getMessage();
268             updateProperty(PROPERTY_MENU_ERROR, message != null ? message : "");
269             // Some AVRs send unexpected responses. We log parser exceptions therefore.
270             logger.debug("Parse error!", e);
271         } finally {
272             loadingDone.countDown();
273         }
274     }
275
276     /**
277      * We handle the update ourselves to avoid a costly dispose/initialize
278      */
279     @Override
280     public void handleConfigurationUpdate(Map<String, Object> configurationParameters) {
281         if (!isInitialized()) {
282             super.handleConfigurationUpdate(configurationParameters);
283             return;
284         }
285
286         validateConfigurationParameters(configurationParameters);
287
288         // can be overridden by subclasses
289         Configuration configurationObject = editConfiguration();
290         for (Entry<String, Object> configurationParameter : configurationParameters.entrySet()) {
291             configurationObject.put(configurationParameter.getKey(), configurationParameter.getValue());
292         }
293
294         updateConfiguration(configurationObject);
295
296         bridgeConfig = configurationObject.as(YamahaBridgeConfig.class);
297         logger.trace("Update configuration of {} with host '{}' and port {}", getThing().getLabel(),
298                 bridgeConfig.getHost(), bridgeConfig.getPort());
299
300         Optional<String> host = bridgeConfig.getHostWithPort();
301         if (host.isPresent()) {
302             connection.setHost(host.get());
303             onConnectionCreated(connection);
304         }
305
306         inputConverter = protocolFactory.InputConverter(connection, bridgeConfig.getInputMapping());
307         setupRefreshTimer(bridgeConfig.getRefreshInterval());
308     }
309
310     /**
311      * We handle updates of this thing ourself.
312      */
313     @Override
314     public void thingUpdated(Thing thing) {
315         this.thing = thing;
316     }
317
318     @Override
319     public Collection<Class<? extends ThingHandlerService>> getServices() {
320         return Collections.singleton(ZoneDiscoveryService.class);
321     }
322
323     /**
324      * Called by the zone discovery service to let this handler have a reference.
325      */
326     public void setZoneDiscoveryService(ZoneDiscoveryService s) {
327         this.zoneDiscoveryService = s;
328     }
329
330     /**
331      * Calls createCommunicationObject if the host name is configured correctly.
332      */
333     @Override
334     public void initialize() {
335         bridgeConfig = getConfigAs(YamahaBridgeConfig.class);
336         logger.trace("Initialize of {} with host '{}' and port {}", getThing().getLabel(), bridgeConfig.getHost(),
337                 bridgeConfig.getPort());
338
339         Optional<String> host = bridgeConfig.getHostWithPort();
340         if (!host.isPresent()) {
341             String msg = "Host or port not set. Double check your thing settings.";
342             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, msg);
343             logger.warn(msg);
344             return;
345         }
346
347         if (zoneDiscoveryService == null) {
348             logger.warn("Zone discovery service not ready!");
349             return;
350         }
351
352         protocolFactory.createConnection(host.get(), this);
353     }
354
355     @Override
356     public void onConnectionCreated(AbstractConnection connection) {
357         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
358                 "Waiting for connection with Yamaha device");
359
360         this.connection = connection;
361         this.systemControl = null;
362
363         if (!ensureConnectionInitialized()) {
364             logger.warn("Communication error. Please review your Yamaha thing configuration.");
365         }
366
367         setupRefreshTimer(0);
368     }
369
370     /**
371      * Attempts to perform a one-time initialization after a connection is created.
372      *
373      * @return true if initialization was successful
374      */
375     private boolean ensureConnectionInitialized() {
376         if (systemControl != null) {
377             return true;
378         }
379
380         logger.trace("Initializing connection");
381
382         try {
383             DeviceInformation deviceInformation = protocolFactory.DeviceInformation(connection, deviceInformationState);
384             deviceInformation.update();
385
386             updateProperty(PROPERTY_VERSION, deviceInformationState.version);
387             updateProperty(PROPERTY_ASSIGNED_NAME, deviceInformationState.name);
388
389             zoneDiscoveryService.publishZones(deviceInformationState, thing.getUID());
390
391             systemControl = protocolFactory.SystemControl(connection, this, deviceInformationState);
392             inputConverter = protocolFactory.InputConverter(connection, bridgeConfig.getInputMapping());
393         } catch (IOException | ReceivedMessageParseException e) {
394             deviceInformationState.invalidate();
395             onConnectivityError(e);
396             return false;
397         }
398         return true;
399     }
400
401     private void onConnectivityError(Exception e) {
402         String description = e.getMessage();
403         logger.debug(
404                 "Communication error. Either the Yamaha thing configuration is invalid or the device is offline. Details: {}",
405                 description);
406         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, description);
407     }
408
409     @Override
410     public void systemControlStateChanged(SystemControlState msg) {
411         // If the device was off and now turns on, we trigger a refresh of all zone things.
412         // The user might have renamed some of the inputs etc.
413         boolean needsCompleteRefresh = msg.power && !systemControlState.power;
414         systemControlState = msg;
415
416         updateState(CHANNEL_POWER, systemControlState.power ? OnOffType.ON : OnOffType.OFF);
417         if (needsCompleteRefresh) {
418             updateAllZoneInformation();
419         }
420     }
421 }