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.yamahareceiver.internal.handler;
15 import static org.openhab.binding.yamahareceiver.internal.YamahaReceiverBindingConstants.*;
17 import java.io.IOException;
18 import java.util.Collection;
19 import java.util.Collections;
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;
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;
56 * The {@link YamahaBridgeHandler} is responsible for fetching basic information about the
57 * found AVR and start the zone detection.
59 * @author David Graeff - Initial contribution
60 * @author Tomasz Maruszak - Input mapping fix, volumeDB fix, better feature detection, added config object
62 public class YamahaBridgeHandler extends BaseBridgeHandler
63 implements ConnectionStateListener, SystemControlStateListener {
65 private final Logger logger = LoggerFactory.getLogger(YamahaBridgeHandler.class);
67 private YamahaBridgeConfig bridgeConfig;
68 private InputConverter inputConverter;
70 private ScheduledFuture<?> refreshTimer;
71 private ZoneDiscoveryService zoneDiscoveryService;
73 private final DeviceInformationState deviceInformationState = new DeviceInformationState();
75 private SystemControl systemControl;
76 private SystemControlState systemControlState = new SystemControlState();
78 private final CountDownLatch loadingDone = new CountDownLatch(1);
80 private boolean disposed = false;
82 private ProtocolFactory protocolFactory;
83 private AbstractConnection connection;
85 public YamahaBridgeHandler(Bridge bridge) {
87 protocolFactory = new XMLProtocolFactory();
91 * Return the input mapping converter
93 public InputConverter getInputConverter() {
94 return inputConverter;
98 * @return Return the protocol communication object. This may be null
99 * if the bridge is offline.
101 public AbstractConnection getConnection() {
106 * Gets the current protocol factory.
110 public ProtocolFactory getProtocolFactory() {
111 return protocolFactory;
115 * Sets the current protocol factory.
117 * @param protocolFactory
119 public void setProtocolFactory(ProtocolFactory protocolFactory) {
120 this.protocolFactory = protocolFactory;
124 public void dispose() {
125 cancelRefreshTimer();
132 * Returns the device information
134 public DeviceInformationState getDeviceInformationState() {
135 return deviceInformationState;
139 * Returns the device configuration
141 public YamahaBridgeConfig getConfiguration() {
146 public void handleCommand(ChannelUID channelUID, Command command) {
147 if (connection == null || deviceInformationState.host == null) {
151 if (command instanceof RefreshType) {
152 refreshFromState(channelUID);
157 // Might be extended in the future, therefore a switch statement
158 String id = channelUID.getId();
161 systemControl.setPower(((OnOffType) command) == OnOffType.ON);
163 case CHANNEL_PARTY_MODE:
164 systemControl.setPartyMode(((OnOffType) command) == OnOffType.ON);
166 case CHANNEL_PARTY_MODE_MUTE:
167 systemControl.setPartyModeMute(((OnOffType) command) == OnOffType.ON);
169 case CHANNEL_PARTY_MODE_VOLUME:
170 if (command instanceof IncreaseDecreaseType increaseDecreaseCommand) {
171 systemControl.setPartyModeVolume(increaseDecreaseCommand == IncreaseDecreaseType.INCREASE);
173 logger.warn("Only {} and {} commands are supported for {}", IncreaseDecreaseType.DECREASE,
174 IncreaseDecreaseType.DECREASE, id);
179 "Channel {} not supported on the yamaha device directly! Try with the zone things instead.",
182 } catch (IOException | ReceivedMessageParseException e) {
183 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
187 private void refreshFromState(ChannelUID channelUID) {
188 // Might be extended in the future, therefore a switch statement
189 switch (channelUID.getId()) {
191 updateState(channelUID, systemControlState.power ? OnOffType.ON : OnOffType.OFF);
193 case CHANNEL_PARTY_MODE:
194 updateState(channelUID, systemControlState.partyMode ? OnOffType.ON : OnOffType.OFF);
196 case CHANNEL_PARTY_MODE_MUTE:
197 case CHANNEL_PARTY_MODE_VOLUME:
198 // no state updates available
201 logger.warn("Channel refresh for {} not implemented!", channelUID.getId());
206 * Sets up a refresh timer (using the scheduler) with the given interval.
208 * @param initialWaitTime The delay before the first refresh. Maybe 0 to immediately
209 * initiate a refresh.
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);
220 * Cancels the refresh timer (if one was setup).
222 private void cancelRefreshTimer() {
223 if (refreshTimer != null) {
224 refreshTimer.cancel(false);
230 * Periodically and initially called. This must run in another thread, because all update calls are blocking.
232 void updateAllZoneInformation() {
234 logger.trace("updateAllZoneInformation will be skipped because the bridge is disposed");
238 if (!ensureConnectionInitialized()) {
239 // The initialization did not yet happen and the device is still offline (or not reachable)
243 logger.trace("updateAllZoneInformation");
245 // Set power = true before calling systemControl.update(),
246 // otherwise the systemControlStateChanged method would call updateAllZoneInformation() again
247 systemControlState.power = true;
248 systemControl.update();
250 updateStatus(ThingStatus.ONLINE);
252 Bridge bridge = (Bridge) thing;
253 for (Thing thing : bridge.getThings()) {
254 YamahaZoneThingHandler handler = (YamahaZoneThingHandler) thing.getHandler();
256 // Ensure the handler has been already assigned
257 if (handler != null && handler.isCorrectlyInitialized()) {
258 handler.updateZoneInformation();
261 } catch (IOException e) {
262 systemControlState.invalidate();
263 onConnectivityError(e);
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);
271 loadingDone.countDown();
276 * We handle the update ourselves to avoid a costly dispose/initialize
279 public void handleConfigurationUpdate(Map<String, Object> configurationParameters) {
280 if (!isInitialized()) {
281 super.handleConfigurationUpdate(configurationParameters);
285 validateConfigurationParameters(configurationParameters);
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());
293 updateConfiguration(configurationObject);
295 bridgeConfig = configurationObject.as(YamahaBridgeConfig.class);
296 logger.trace("Update configuration of {} with host '{}' and port {}", getThing().getLabel(),
297 bridgeConfig.getHost(), bridgeConfig.getPort());
299 Optional<String> host = bridgeConfig.getHostWithPort();
300 if (host.isPresent()) {
301 connection.setHost(host.get());
302 onConnectionCreated(connection);
305 inputConverter = protocolFactory.InputConverter(connection, bridgeConfig.getInputMapping());
306 setupRefreshTimer(bridgeConfig.getRefreshInterval());
310 * We handle updates of this thing ourself.
313 public void thingUpdated(Thing thing) {
318 public Collection<Class<? extends ThingHandlerService>> getServices() {
319 return Collections.singleton(ZoneDiscoveryService.class);
323 * Called by the zone discovery service to let this handler have a reference.
325 public void setZoneDiscoveryService(ZoneDiscoveryService s) {
326 this.zoneDiscoveryService = s;
330 * Calls createCommunicationObject if the host name is configured correctly.
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());
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);
346 if (zoneDiscoveryService == null) {
347 logger.warn("Zone discovery service not ready!");
351 protocolFactory.createConnection(host.get(), this);
355 public void onConnectionCreated(AbstractConnection connection) {
356 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
357 "Waiting for connection with Yamaha device");
359 this.connection = connection;
360 this.systemControl = null;
362 if (!ensureConnectionInitialized()) {
363 logger.warn("Communication error. Please review your Yamaha thing configuration.");
366 setupRefreshTimer(0);
370 * Attempts to perform a one-time initialization after a connection is created.
372 * @return true if initialization was successful
374 private boolean ensureConnectionInitialized() {
375 if (systemControl != null) {
379 logger.trace("Initializing connection");
382 DeviceInformation deviceInformation = protocolFactory.DeviceInformation(connection, deviceInformationState);
383 deviceInformation.update();
385 updateProperty(PROPERTY_VERSION, deviceInformationState.version);
386 updateProperty(PROPERTY_ASSIGNED_NAME, deviceInformationState.name);
388 zoneDiscoveryService.publishZones(deviceInformationState, thing.getUID());
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);
400 private void onConnectivityError(Exception e) {
401 String description = e.getMessage();
403 "Communication error. Either the Yamaha thing configuration is invalid or the device is offline. Details: {}",
405 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, description);
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;
415 updateState(CHANNEL_POWER, systemControlState.power ? OnOffType.ON : OnOffType.OFF);
416 if (needsCompleteRefresh) {
417 updateAllZoneInformation();