]> git.basschouten.com Git - openhab-addons.git/blob
56dc785f09d011c7ee08f30887d8bab81dc58ba1
[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.neeo.internal.handler;
14
15 import java.io.IOException;
16 import java.util.Arrays;
17 import java.util.HashMap;
18 import java.util.Map;
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;
25
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;
55
56 /**
57  * An extension of {@link BaseThingHandler} that is responsible for handling commands for a device
58  *
59  * @author Tim Roberts - Initial contribution
60  */
61 @NonNullByDefault
62 public class NeeoDeviceHandler extends BaseThingHandler {
63
64     /** The logger */
65     private final Logger logger = LoggerFactory.getLogger(NeeoDeviceHandler.class);
66
67     /**
68      * The initialization task (null until set by {@link #initializeTask()} and set back to null in {@link #dispose()}
69      */
70     private final AtomicReference<@Nullable Future<?>> initializationTask = new AtomicReference<>(null);
71
72     /**
73      * The refresh task (null until set by {@link #initializeTask()} and set back to null in {@link #dispose()}
74      */
75     private final AtomicReference<@Nullable ScheduledFuture<?>> refreshTask = new AtomicReference<>(null);
76
77     /** The {@link NeeoDeviceProtocol} (null until set by {@link #initializationTask}) */
78     private final AtomicReference<@Nullable NeeoDeviceProtocol> deviceProtocol = new AtomicReference<>();
79
80     /**
81      * Instantiates a new neeo device handler.
82      *
83      * @param thing the non-null thing
84      */
85     NeeoDeviceHandler(Thing thing) {
86         super(thing);
87         Objects.requireNonNull(thing, "thing cannot be null");
88     }
89
90     @Override
91     public void handleCommand(ChannelUID channelUID, Command command) {
92         Objects.requireNonNull(channelUID, "channelUID cannot be null");
93         Objects.requireNonNull(command, "command cannot be null");
94
95         final NeeoDeviceProtocol protocol = deviceProtocol.get();
96         if (protocol == null) {
97             logger.debug("Protocol is null - ignoring update: {}", channelUID);
98             return;
99         }
100
101         final String[] channelIds = UidUtils.parseChannelId(channelUID);
102         if (channelIds.length == 0) {
103             logger.debug("Bad group declaration: {}", channelUID);
104             return;
105         }
106
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] : "";
111
112         if (groupId.isEmpty()) {
113             logger.debug("GroupID for channel is null - ignoring command: {}", channelUID);
114             return;
115         }
116
117         if (command instanceof RefreshType) {
118             refreshChannel(protocol, groupId, channelId, channelKey);
119         } else {
120             switch (groupId) {
121                 case NeeoConstants.DEVICE_GROUP_MACROS_ID:
122                     switch (channelId) {
123                         case NeeoConstants.DEVICE_CHANNEL_STATUS:
124                             if (command instanceof OnOffType) {
125                                 protocol.setMacroStatus(channelKey, command == OnOffType.ON);
126                             }
127                             break;
128                         default:
129                             logger.debug("Unknown channel to set: {}", channelUID);
130                             break;
131                     }
132                     break;
133                 default:
134                     logger.debug("Unknown group to set: {}", channelUID);
135                     break;
136             }
137         }
138     }
139
140     /**
141      * Refresh the specified channel section, key and id using the specified protocol
142      *
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
147      */
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");
153
154         switch (groupId) {
155             case NeeoConstants.DEVICE_GROUP_MACROS_ID:
156                 switch (channelId) {
157                     case NeeoConstants.DEVICE_CHANNEL_STATUS:
158                         protocol.refreshMacroStatus(channelKey);
159                         break;
160                 }
161                 break;
162         }
163     }
164
165     @Override
166     public void initialize() {
167         NeeoUtil.cancel(initializationTask.getAndSet(scheduler.submit(() -> {
168             initializeTask();
169         })));
170     }
171
172     /**
173      * Initializes the task be creating the {@link NeeoDeviceProtocol}, going online and then scheduling the refresh
174      * task.
175      */
176     private void initializeTask() {
177         final NeeoDeviceConfig config = getConfigAs(NeeoDeviceConfig.class);
178
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");
183             return;
184         }
185
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");
190             return;
191         }
192
193         try {
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");
198                 return;
199             }
200
201             final NeeoRoom room = brainApi.getRoom(roomKey);
202
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 + ")");
207                 return;
208             }
209
210             final ThingUID thingUid = getThing().getUID();
211
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());
221
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()));
227                 }
228
229                 properties.put("Device Capabilities",
230                         Arrays.stream(details.getDeviceCapabilities()).collect(Collectors.joining(",")));
231             }
232
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());
237
238             NeeoUtil.checkInterrupt();
239             final NeeoDeviceProtocol protocol = new NeeoDeviceProtocol(new NeeoHandlerCallback() {
240
241                 @Override
242                 public void statusChanged(ThingStatus status, ThingStatusDetail detail, String msg) {
243                     updateStatus(status, detail, msg);
244                 }
245
246                 @Override
247                 public void stateChanged(String channelId, State state) {
248                     updateState(channelId, state);
249                 }
250
251                 @Override
252                 public void setProperty(String propertyName, String propertyValue) {
253                     getThing().setProperty(propertyName, propertyValue);
254                 }
255
256                 @Override
257                 public void scheduleTask(Runnable task, long milliSeconds) {
258                     scheduler.schedule(task, milliSeconds, TimeUnit.MILLISECONDS);
259                 }
260
261                 @Override
262                 public void triggerEvent(String channelID, String event) {
263                     triggerChannel(channelID, event);
264                 }
265
266                 @Nullable
267                 @Override
268                 public NeeoBrainApi getApi() {
269                     return getNeeoBrainApi();
270                 }
271             }, roomKey, deviceKey);
272             deviceProtocol.getAndSet(protocol);
273
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");
284         }
285     }
286
287     /**
288      * Helper method to add a property to the properties map if the value is not null
289      *
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
293      */
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);
299         }
300     }
301
302     /**
303      * Helper method to get the room key from the parent bridge (which should be a room)
304      *
305      * @return a non-null, non-empty room key if found, null if not found
306      */
307     @Nullable
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();
314             }
315         }
316         return null;
317     }
318
319     /**
320      * Helper method to simply create a string from an integer
321      *
322      * @param i the integer
323      * @return the resulting string representation
324      */
325     private static String toString(@Nullable Integer i) {
326         if (i == null) {
327             return "";
328         }
329         return i.toString();
330     }
331
332     /**
333      * Helper method to return the {@link NeeoRoomHandler} associated with this handler
334      *
335      * @return a possibly null {@link NeeoRoomHandler}
336      */
337     @Nullable
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);
344             }
345         }
346         return null;
347     }
348
349     /**
350      * Returns the {@link NeeoBrainApi} associated with this handler.
351      *
352      * @return the {@link NeeoBrainApi} or null if not found
353      */
354     @Nullable
355     private NeeoBrainApi getNeeoBrainApi() {
356         final NeeoRoomHandler handler = getRoomHandler();
357         return handler == null ? null : handler.getNeeoBrainApi();
358     }
359
360     /**
361      * Returns the brain ID associated with this handler.
362      *
363      * @return the brain ID or null if not found
364      */
365     @Nullable
366     public String getNeeoBrainId() {
367         final NeeoRoomHandler handler = getRoomHandler();
368         return handler == null ? null : handler.getNeeoBrainId();
369     }
370
371     @Override
372     public void dispose() {
373         NeeoUtil.cancel(initializationTask.getAndSet(null));
374         NeeoUtil.cancel(refreshTask.getAndSet(null));
375         deviceProtocol.getAndSet(null);
376     }
377 }