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.neeo.internal.handler;
15 import java.io.IOException;
16 import java.util.Arrays;
17 import java.util.HashMap;
19 import java.util.Objects;
20 import java.util.concurrent.Future;
21 import java.util.concurrent.ScheduledFuture;
22 import java.util.concurrent.TimeUnit;
23 import java.util.concurrent.atomic.AtomicReference;
24 import java.util.stream.Collectors;
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.openhab.binding.neeo.internal.NeeoBrainApi;
29 import org.openhab.binding.neeo.internal.NeeoConstants;
30 import org.openhab.binding.neeo.internal.NeeoDeviceConfig;
31 import org.openhab.binding.neeo.internal.NeeoDeviceProtocol;
32 import org.openhab.binding.neeo.internal.NeeoHandlerCallback;
33 import org.openhab.binding.neeo.internal.NeeoRoomConfig;
34 import org.openhab.binding.neeo.internal.NeeoUtil;
35 import org.openhab.binding.neeo.internal.UidUtils;
36 import org.openhab.binding.neeo.internal.models.NeeoDevice;
37 import org.openhab.binding.neeo.internal.models.NeeoDeviceDetails;
38 import org.openhab.binding.neeo.internal.models.NeeoDeviceDetailsTiming;
39 import org.openhab.binding.neeo.internal.models.NeeoRoom;
40 import org.openhab.core.library.types.OnOffType;
41 import org.openhab.core.thing.Bridge;
42 import org.openhab.core.thing.ChannelUID;
43 import org.openhab.core.thing.Thing;
44 import org.openhab.core.thing.ThingStatus;
45 import org.openhab.core.thing.ThingStatusDetail;
46 import org.openhab.core.thing.ThingUID;
47 import org.openhab.core.thing.binding.BaseThingHandler;
48 import org.openhab.core.thing.binding.BridgeHandler;
49 import org.openhab.core.thing.binding.builder.ThingBuilder;
50 import org.openhab.core.types.Command;
51 import org.openhab.core.types.RefreshType;
52 import org.openhab.core.types.State;
53 import org.slf4j.Logger;
54 import org.slf4j.LoggerFactory;
57 * An extension of {@link BaseThingHandler} that is responsible for handling commands for a device
59 * @author Tim Roberts - Initial contribution
62 public class NeeoDeviceHandler extends BaseThingHandler {
65 private final Logger logger = LoggerFactory.getLogger(NeeoDeviceHandler.class);
68 * The initialization task (null until set by {@link #initializeTask()} and set back to null in {@link #dispose()}
70 private final AtomicReference<@Nullable Future<?>> initializationTask = new AtomicReference<>(null);
73 * The refresh task (null until set by {@link #initializeTask()} and set back to null in {@link #dispose()}
75 private final AtomicReference<@Nullable ScheduledFuture<?>> refreshTask = new AtomicReference<>(null);
77 /** The {@link NeeoDeviceProtocol} (null until set by {@link #initializationTask}) */
78 private final AtomicReference<@Nullable NeeoDeviceProtocol> deviceProtocol = new AtomicReference<>();
81 * Instantiates a new neeo device handler.
83 * @param thing the non-null thing
85 NeeoDeviceHandler(Thing thing) {
87 Objects.requireNonNull(thing, "thing cannot be null");
91 public void handleCommand(ChannelUID channelUID, Command command) {
92 Objects.requireNonNull(channelUID, "channelUID cannot be null");
93 Objects.requireNonNull(command, "command cannot be null");
95 final NeeoDeviceProtocol protocol = deviceProtocol.get();
96 if (protocol == null) {
97 logger.debug("Protocol is null - ignoring update: {}", channelUID);
101 final String[] channelIds = UidUtils.parseChannelId(channelUID);
102 if (channelIds.length == 0) {
103 logger.debug("Bad group declaration: {}", channelUID);
107 final String localGroupId = channelUID.getGroupId();
108 final String groupId = localGroupId == null || localGroupId.isEmpty() ? "" : localGroupId;
109 final String channelId = channelIds[0];
110 final String channelKey = channelIds.length > 1 ? channelIds[1] : "";
112 if (groupId.isEmpty()) {
113 logger.debug("GroupID for channel is null - ignoring command: {}", channelUID);
117 if (command instanceof RefreshType) {
118 refreshChannel(protocol, groupId, channelId, channelKey);
121 case NeeoConstants.DEVICE_GROUP_MACROS_ID:
123 case NeeoConstants.DEVICE_CHANNEL_STATUS:
124 if (command instanceof OnOffType) {
125 protocol.setMacroStatus(channelKey, command == OnOffType.ON);
129 logger.debug("Unknown channel to set: {}", channelUID);
134 logger.debug("Unknown group to set: {}", channelUID);
141 * Refresh the specified channel section, key and id using the specified protocol
143 * @param protocol a non-null protocol to use
144 * @param groupId the non-empty groupId
145 * @param channelId the non-empty channel id
146 * @param channelKey the non-empty channel key
148 private void refreshChannel(NeeoDeviceProtocol protocol, String groupId, String channelId, String channelKey) {
149 Objects.requireNonNull(protocol, "protocol cannot be null");
150 NeeoUtil.requireNotEmpty(groupId, "groupId must not be empty");
151 NeeoUtil.requireNotEmpty(channelId, "channelId must not be empty");
152 NeeoUtil.requireNotEmpty(channelKey, "channelKey must not be empty");
155 case NeeoConstants.DEVICE_GROUP_MACROS_ID:
157 case NeeoConstants.DEVICE_CHANNEL_STATUS:
158 protocol.refreshMacroStatus(channelKey);
166 public void initialize() {
167 NeeoUtil.cancel(initializationTask.getAndSet(scheduler.submit(() -> {
173 * Initializes the task be creating the {@link NeeoDeviceProtocol}, going online and then scheduling the refresh
176 private void initializeTask() {
177 final NeeoDeviceConfig config = getConfigAs(NeeoDeviceConfig.class);
179 final String roomKey = getRoomKey();
180 if (roomKey == null || roomKey.isEmpty()) {
181 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
182 "Room key (from the parent room bridge) was not found");
186 final String deviceKey = config.getDeviceKey();
187 if (deviceKey == null || deviceKey.isEmpty()) {
188 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
189 "Device key was not found or empty");
194 NeeoUtil.checkInterrupt();
195 final NeeoBrainApi brainApi = getNeeoBrainApi();
196 if (brainApi == null) {
197 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, "Cannot find the NEEO Brain API");
201 final NeeoRoom room = brainApi.getRoom(roomKey);
203 final NeeoDevice device = room.getDevices().getDevice(deviceKey);
204 if (device == null) {
205 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
206 "Device (" + config.getDeviceKey() + ") was not found in room (" + roomKey + ")");
210 final ThingUID thingUid = getThing().getUID();
212 final Map<String, String> properties = new HashMap<>();
213 final NeeoDeviceDetails details = device.getDetails();
214 if (details != null) {
215 /** The following properties have matches in org.openhab.io.neeo.OpenHabToDeviceConverter.java */
216 addProperty(properties, "Source Name", details.getSourceName());
217 addProperty(properties, "Adapter Name", details.getAdapterName());
218 addProperty(properties, "Type", details.getType());
219 addProperty(properties, "Manufacturer", details.getManufacturer());
220 addProperty(properties, "Name", details.getName());
222 final NeeoDeviceDetailsTiming timing = details.getTiming();
223 if (timing != null) {
224 properties.put("Standby Command Delay", toString(timing.getStandbyCommandDelay()));
225 properties.put("Source Switch Delay", toString(timing.getSourceSwitchDelay()));
226 properties.put("Shutdown Delay", toString(timing.getShutdownDelay()));
229 properties.put("Device Capabilities",
230 Arrays.stream(details.getDeviceCapabilities()).collect(Collectors.joining(",")));
233 final ThingBuilder thingBuilder = editThing();
234 thingBuilder.withLabel(device.getName() + " (NEEO " + brainApi.getBrain().getKey() + ")")
235 .withProperties(properties).withChannels(ChannelUtils.generateChannels(thingUid, device));
236 updateThing(thingBuilder.build());
238 NeeoUtil.checkInterrupt();
239 final NeeoDeviceProtocol protocol = new NeeoDeviceProtocol(new NeeoHandlerCallback() {
242 public void statusChanged(ThingStatus status, ThingStatusDetail detail, String msg) {
243 updateStatus(status, detail, msg);
247 public void stateChanged(String channelId, State state) {
248 updateState(channelId, state);
252 public void setProperty(String propertyName, String propertyValue) {
253 getThing().setProperty(propertyName, propertyValue);
257 public void scheduleTask(Runnable task, long milliSeconds) {
258 scheduler.schedule(task, milliSeconds, TimeUnit.MILLISECONDS);
262 public void triggerEvent(String channelID, String event) {
263 triggerChannel(channelID, event);
268 public NeeoBrainApi getApi() {
269 return getNeeoBrainApi();
271 }, roomKey, deviceKey);
272 deviceProtocol.getAndSet(protocol);
274 NeeoUtil.checkInterrupt();
275 updateStatus(ThingStatus.ONLINE);
276 } catch (IOException e) {
277 logger.debug("IOException during initialization", e);
278 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
279 "Room " + roomKey + " couldn't be found");
280 } catch (InterruptedException e) {
281 logger.debug("Initialization was interrupted", e);
282 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_INITIALIZING_ERROR,
283 "Initialization was interrupted");
288 * Helper method to add a property to the properties map if the value is not null
290 * @param properties a non-null properties map
291 * @param key a non-null, non-empty key
292 * @param value a possibly null, possibly empty key
294 private void addProperty(Map<String, String> properties, String key, @Nullable String value) {
295 Objects.requireNonNull(properties, "properties cannot be null");
296 NeeoUtil.requireNotEmpty(key, "key cannot be empty");
297 if (value != null && !value.isEmpty()) {
298 properties.put(key, value);
303 * Helper method to get the room key from the parent bridge (which should be a room)
305 * @return a non-null, non-empty room key if found, null if not found
308 private String getRoomKey() {
309 final Bridge bridge = getBridge();
310 if (bridge != null) {
311 final BridgeHandler handler = bridge.getHandler();
312 if (handler instanceof NeeoRoomHandler) {
313 return handler.getThing().getConfiguration().as(NeeoRoomConfig.class).getRoomKey();
320 * Helper method to simply create a string from an integer
322 * @param i the integer
323 * @return the resulting string representation
325 private static String toString(@Nullable Integer i) {
333 * Helper method to return the {@link NeeoRoomHandler} associated with this handler
335 * @return a possibly null {@link NeeoRoomHandler}
338 private NeeoRoomHandler getRoomHandler() {
339 final Bridge parent = getBridge();
340 if (parent != null) {
341 final BridgeHandler handler = parent.getHandler();
342 if (handler instanceof NeeoRoomHandler) {
343 return ((NeeoRoomHandler) handler);
350 * Returns the {@link NeeoBrainApi} associated with this handler.
352 * @return the {@link NeeoBrainApi} or null if not found
355 private NeeoBrainApi getNeeoBrainApi() {
356 final NeeoRoomHandler handler = getRoomHandler();
357 return handler == null ? null : handler.getNeeoBrainApi();
361 * Returns the brain ID associated with this handler.
363 * @return the brain ID or null if not found
366 public String getNeeoBrainId() {
367 final NeeoRoomHandler handler = getRoomHandler();
368 return handler == null ? null : handler.getNeeoBrainId();
372 public void dispose() {
373 NeeoUtil.cancel(initializationTask.getAndSet(null));
374 NeeoUtil.cancel(refreshTask.getAndSet(null));
375 deviceProtocol.getAndSet(null);