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) {
172 .setPartyModeVolume(((IncreaseDecreaseType) command) == IncreaseDecreaseType.INCREASE);
174 logger.warn("Only {} and {} commands are supported for {}", IncreaseDecreaseType.DECREASE,
175 IncreaseDecreaseType.DECREASE, id);
180 "Channel {} not supported on the yamaha device directly! Try with the zone things instead.",
183 } catch (IOException | ReceivedMessageParseException e) {
184 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
188 private void refreshFromState(ChannelUID channelUID) {
189 // Might be extended in the future, therefore a switch statement
190 switch (channelUID.getId()) {
192 updateState(channelUID, systemControlState.power ? OnOffType.ON : OnOffType.OFF);
194 case CHANNEL_PARTY_MODE:
195 updateState(channelUID, systemControlState.partyMode ? OnOffType.ON : OnOffType.OFF);
197 case CHANNEL_PARTY_MODE_MUTE:
198 case CHANNEL_PARTY_MODE_VOLUME:
199 // no state updates available
202 logger.warn("Channel refresh for {} not implemented!", channelUID.getId());
207 * Sets up a refresh timer (using the scheduler) with the given interval.
209 * @param initialWaitTime The delay before the first refresh. Maybe 0 to immediately
210 * initiate a refresh.
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);
221 * Cancels the refresh timer (if one was setup).
223 private void cancelRefreshTimer() {
224 if (refreshTimer != null) {
225 refreshTimer.cancel(false);
231 * Periodically and initially called. This must run in another thread, because all update calls are blocking.
233 void updateAllZoneInformation() {
235 logger.trace("updateAllZoneInformation will be skipped because the bridge is disposed");
239 if (!ensureConnectionInitialized()) {
240 // The initialization did not yet happen and the device is still offline (or not reachable)
244 logger.trace("updateAllZoneInformation");
246 // Set power = true before calling systemControl.update(),
247 // otherwise the systemControlStateChanged method would call updateAllZoneInformation() again
248 systemControlState.power = true;
249 systemControl.update();
251 updateStatus(ThingStatus.ONLINE);
253 Bridge bridge = (Bridge) thing;
254 for (Thing thing : bridge.getThings()) {
255 YamahaZoneThingHandler handler = (YamahaZoneThingHandler) thing.getHandler();
257 // Ensure the handler has been already assigned
258 if (handler != null && handler.isCorrectlyInitialized()) {
259 handler.updateZoneInformation();
262 } catch (IOException e) {
263 systemControlState.invalidate();
264 onConnectivityError(e);
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);
272 loadingDone.countDown();
277 * We handle the update ourselves to avoid a costly dispose/initialize
280 public void handleConfigurationUpdate(Map<String, Object> configurationParameters) {
281 if (!isInitialized()) {
282 super.handleConfigurationUpdate(configurationParameters);
286 validateConfigurationParameters(configurationParameters);
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());
294 updateConfiguration(configurationObject);
296 bridgeConfig = configurationObject.as(YamahaBridgeConfig.class);
297 logger.trace("Update configuration of {} with host '{}' and port {}", getThing().getLabel(),
298 bridgeConfig.getHost(), bridgeConfig.getPort());
300 Optional<String> host = bridgeConfig.getHostWithPort();
301 if (host.isPresent()) {
302 connection.setHost(host.get());
303 onConnectionCreated(connection);
306 inputConverter = protocolFactory.InputConverter(connection, bridgeConfig.getInputMapping());
307 setupRefreshTimer(bridgeConfig.getRefreshInterval());
311 * We handle updates of this thing ourself.
314 public void thingUpdated(Thing thing) {
319 public Collection<Class<? extends ThingHandlerService>> getServices() {
320 return Collections.singleton(ZoneDiscoveryService.class);
324 * Called by the zone discovery service to let this handler have a reference.
326 public void setZoneDiscoveryService(ZoneDiscoveryService s) {
327 this.zoneDiscoveryService = s;
331 * Calls createCommunicationObject if the host name is configured correctly.
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());
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);
347 if (zoneDiscoveryService == null) {
348 logger.warn("Zone discovery service not ready!");
352 protocolFactory.createConnection(host.get(), this);
356 public void onConnectionCreated(AbstractConnection connection) {
357 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_PENDING,
358 "Waiting for connection with Yamaha device");
360 this.connection = connection;
361 this.systemControl = null;
363 if (!ensureConnectionInitialized()) {
364 logger.warn("Communication error. Please review your Yamaha thing configuration.");
367 setupRefreshTimer(0);
371 * Attempts to perform a one-time initialization after a connection is created.
373 * @return true if initialization was successful
375 private boolean ensureConnectionInitialized() {
376 if (systemControl != null) {
380 logger.trace("Initializing connection");
383 DeviceInformation deviceInformation = protocolFactory.DeviceInformation(connection, deviceInformationState);
384 deviceInformation.update();
386 updateProperty(PROPERTY_VERSION, deviceInformationState.version);
387 updateProperty(PROPERTY_ASSIGNED_NAME, deviceInformationState.name);
389 zoneDiscoveryService.publishZones(deviceInformationState, thing.getUID());
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);
401 private void onConnectivityError(Exception e) {
402 String description = e.getMessage();
404 "Communication error. Either the Yamaha thing configuration is invalid or the device is offline. Details: {}",
406 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, description);
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;
416 updateState(CHANNEL_POWER, systemControlState.power ? OnOffType.ON : OnOffType.OFF);
417 if (needsCompleteRefresh) {
418 updateAllZoneInformation();