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.harmonyhub.internal.handler;
15 import static org.openhab.binding.harmonyhub.internal.HarmonyHubBindingConstants.*;
17 import java.util.ArrayList;
18 import java.util.Collections;
19 import java.util.LinkedList;
20 import java.util.List;
23 import org.eclipse.jdt.annotation.NonNullByDefault;
24 import org.eclipse.jdt.annotation.Nullable;
25 import org.openhab.binding.harmonyhub.internal.HarmonyHubDynamicTypeProvider;
26 import org.openhab.binding.harmonyhub.internal.config.HarmonyDeviceConfig;
27 import org.openhab.core.library.types.StringType;
28 import org.openhab.core.thing.Bridge;
29 import org.openhab.core.thing.Channel;
30 import org.openhab.core.thing.ChannelUID;
31 import org.openhab.core.thing.Thing;
32 import org.openhab.core.thing.ThingStatus;
33 import org.openhab.core.thing.ThingStatusDetail;
34 import org.openhab.core.thing.ThingStatusInfo;
35 import org.openhab.core.thing.ThingTypeUID;
36 import org.openhab.core.thing.binding.BaseThingHandler;
37 import org.openhab.core.thing.binding.builder.ChannelBuilder;
38 import org.openhab.core.thing.binding.builder.ThingBuilder;
39 import org.openhab.core.thing.type.ChannelType;
40 import org.openhab.core.thing.type.ChannelTypeBuilder;
41 import org.openhab.core.thing.type.ChannelTypeUID;
42 import org.openhab.core.types.Command;
43 import org.openhab.core.types.RefreshType;
44 import org.openhab.core.types.StateDescriptionFragmentBuilder;
45 import org.openhab.core.types.StateOption;
46 import org.openhab.core.types.UnDefType;
47 import org.slf4j.Logger;
48 import org.slf4j.LoggerFactory;
50 import com.digitaldan.harmony.config.ControlGroup;
51 import com.digitaldan.harmony.config.Device;
52 import com.digitaldan.harmony.config.Function;
53 import com.digitaldan.harmony.config.HarmonyConfig;
56 * The {@link HarmonyDeviceHandler} is responsible for handling commands for Harmony Devices, which are
57 * sent to one of the channels. It also is responsible for dynamically creating the button press channel
58 * based on the device's available button press functions.
60 * @author Dan Cunningham - Initial contribution
61 * @author Wouter Born - Add null annotations
64 public class HarmonyDeviceHandler extends BaseThingHandler {
66 private final Logger logger = LoggerFactory.getLogger(HarmonyDeviceHandler.class);
68 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections.singleton(HARMONY_DEVICE_THING_TYPE);
70 private final HarmonyHubDynamicTypeProvider typeProvider;
72 private @NonNullByDefault({}) HarmonyDeviceConfig config;
74 public HarmonyDeviceHandler(Thing thing, HarmonyHubDynamicTypeProvider typeProvider) {
76 this.typeProvider = typeProvider;
79 protected @Nullable HarmonyHubHandler getHarmonyHubHandler() {
80 Bridge bridge = getBridge();
81 return bridge != null ? (HarmonyHubHandler) bridge.getHandler() : null;
85 public void handleCommand(ChannelUID channelUID, Command command) {
86 logger.trace("Handling command '{}' for {}", command, channelUID);
88 if (command instanceof RefreshType) {
93 if (getThing().getStatus() != ThingStatus.ONLINE) {
94 logger.debug("Hub is offline, ignoring command {} for channel {}", command, channelUID);
98 if (!(command instanceof StringType)) {
99 logger.warn("Command '{}' is not a String type for channel {}", command, channelUID);
103 HarmonyHubHandler hubHandler = getHarmonyHubHandler();
104 if (hubHandler == null) {
105 logger.warn("Command '{}' cannot be handled because {} has no bridge", command, getThing().getUID());
110 String name = config.name;
111 String message = "Pressing button '{}' on {}";
113 logger.debug(message, command, id);
114 hubHandler.pressButton(id, command.toString());
115 } else if (name != null) {
116 logger.debug(message, command, name);
117 hubHandler.pressButton(name, command.toString());
119 logger.warn("Command '{}' cannot be handled because {} has no valid id or name configured", command,
120 getThing().getUID());
122 // may need to ask the list if this can be set here?
123 updateState(channelUID, UnDefType.UNDEF);
127 public void initialize() {
128 config = getConfigAs(HarmonyDeviceConfig.class);
129 boolean validConfiguration = config.name != null || config.id >= 0;
130 if (validConfiguration) {
131 updateStatus(ThingStatus.UNKNOWN);
132 updateBridgeStatus();
134 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
135 "A harmony device thing must be configured with a device name OR a postive device id");
140 public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
141 updateBridgeStatus();
145 public void handleRemoval() {
146 typeProvider.removeChannelTypesForThing(getThing().getUID());
147 super.handleRemoval();
151 * Updates our state based on the bridge/hub
153 private void updateBridgeStatus() {
154 Bridge bridge = getBridge();
155 ThingStatus bridgeStatus = bridge != null ? bridge.getStatus() : null;
156 HarmonyHubHandler hubHandler = getHarmonyHubHandler();
158 boolean bridgeOnline = bridgeStatus == ThingStatus.ONLINE;
159 boolean thingOnline = getThing().getStatus() == ThingStatus.ONLINE;
161 if (bridgeOnline && hubHandler != null && !thingOnline) {
162 updateStatus(ThingStatus.ONLINE);
163 hubHandler.getConfigFuture().thenAcceptAsync(this::updateButtonPressChannel, scheduler).exceptionally(e -> {
164 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
165 "Getting config failed: " + e.getMessage());
168 } else if (!bridgeOnline || hubHandler == null) {
169 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
174 * Updates the buttonPress channel with the available buttons as option states.
176 private void updateButtonPressChannel(@Nullable HarmonyConfig harmonyConfig) {
177 ChannelTypeUID channelTypeUID = new ChannelTypeUID(
178 getThing().getUID().getAsString() + ":" + CHANNEL_BUTTON_PRESS);
180 if (harmonyConfig == null) {
181 logger.debug("Cannot update {} when HarmonyConfig is null", channelTypeUID);
185 logger.debug("Updating {}", channelTypeUID);
187 List<StateOption> states = getButtonStateOptions(harmonyConfig);
189 ChannelType channelType = ChannelTypeBuilder.state(channelTypeUID, "Send Button Press", "String")
190 .withDescription("Send a button press to device " + getThing().getLabel())
191 .withStateDescriptionFragment(StateDescriptionFragmentBuilder.create().withOptions(states).build())
194 typeProvider.putChannelType(channelType);
196 Channel channel = ChannelBuilder.create(new ChannelUID(getThing().getUID(), CHANNEL_BUTTON_PRESS), "String")
197 .withType(channelTypeUID).build();
199 // replace existing buttonPress with updated one
200 List<Channel> newChannels = new ArrayList<>();
201 for (Channel c : getThing().getChannels()) {
202 if (!c.getUID().equals(channel.getUID())) {
206 newChannels.add(channel);
208 ThingBuilder thingBuilder = editThing();
209 thingBuilder.withChannels(newChannels);
210 updateThing(thingBuilder.build());
213 private List<StateOption> getButtonStateOptions(HarmonyConfig harmonyConfig) {
215 String name = config.name;
216 List<StateOption> states = new LinkedList<>();
218 // Iterate through button function commands and add them to our state list
219 for (Device device : harmonyConfig.getDevices()) {
220 boolean sameId = name == null && device.getId() == id;
221 boolean sameName = name != null && name.equals(device.getLabel());
223 if (sameId || sameName) {
224 for (ControlGroup controlGroup : device.getControlGroup()) {
225 for (Function function : controlGroup.getFunction()) {
226 states.add(new StateOption(function.getName(), function.getLabel()));