]> git.basschouten.com Git - openhab-addons.git/blob
03a2a52f7daf8f2ca3b6904a82c3c69a5d26d2d2
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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.insteon.internal.device;
14
15 import java.io.FileInputStream;
16 import java.io.FileNotFoundException;
17 import java.io.IOException;
18 import java.io.InputStream;
19 import java.util.*;
20
21 import org.eclipse.jdt.annotation.NonNullByDefault;
22 import org.eclipse.jdt.annotation.Nullable;
23 import org.openhab.binding.insteon.internal.config.InsteonChannelConfiguration;
24 import org.openhab.binding.insteon.internal.device.DeviceFeatureListener.StateChangeType;
25 import org.openhab.binding.insteon.internal.message.Msg;
26 import org.openhab.binding.insteon.internal.utils.Utils.ParsingException;
27 import org.openhab.core.types.Command;
28 import org.openhab.core.types.State;
29 import org.slf4j.Logger;
30 import org.slf4j.LoggerFactory;
31
32 /**
33  * A DeviceFeature represents a certain feature (trait) of a given Insteon device, e.g. something
34  * operating under a given InsteonAddress that can be manipulated (relay) or read (sensor).
35  *
36  * The DeviceFeature does the processing of incoming messages, and handles commands for the
37  * particular feature it represents.
38  *
39  * It uses four mechanisms for that:
40  *
41  * 1) MessageDispatcher: makes high level decisions about an incoming message and then runs the
42  * 2) MessageHandler: further processes the message, updates state etc
43  * 3) CommandHandler: translates commands from the openhab bus into an Insteon message.
44  * 4) PollHandler: creates an Insteon message to query the DeviceFeature
45  *
46  * Lastly, DeviceFeatureListeners can register with the DeviceFeature to get notifications when
47  * the state of a feature has changed. In practice, a DeviceFeatureListener corresponds to an
48  * openHAB item.
49  *
50  * The character of a DeviceFeature is thus given by a set of message and command handlers.
51  * A FeatureTemplate captures exactly that: it says what set of handlers make up a DeviceFeature.
52  *
53  * DeviceFeatures are added to a new device by referencing a FeatureTemplate (defined in device_features.xml)
54  * from the Device definition file (device_types.xml).
55  *
56  * @author Daniel Pfrommer - Initial contribution
57  * @author Bernd Pfrommer - openHAB 1 insteonplm binding
58  * @author Rob Nielsen - Port to openHAB 2 insteon binding
59  */
60 @NonNullByDefault
61 public class DeviceFeature {
62     public static enum QueryStatus {
63         NEVER_QUERIED,
64         QUERY_PENDING,
65         QUERY_ANSWERED
66     }
67
68     private static final Logger logger = LoggerFactory.getLogger(DeviceFeature.class);
69
70     private static Map<String, FeatureTemplate> features = new HashMap<>();
71
72     private InsteonDevice device = new InsteonDevice();
73     private String name = "INVALID_FEATURE_NAME";
74     private boolean isStatus = false;
75     private int directAckTimeout = 6000;
76     private QueryStatus queryStatus = QueryStatus.NEVER_QUERIED;
77
78     private MessageHandler defaultMsgHandler = new MessageHandler.DefaultMsgHandler(this);
79     private CommandHandler defaultCommandHandler = new CommandHandler.WarnCommandHandler(this);
80     private @Nullable PollHandler pollHandler = null;
81     private @Nullable MessageDispatcher dispatcher = null;
82
83     private Map<Integer, @Nullable MessageHandler> msgHandlers = new HashMap<>();
84     private Map<Class<? extends Command>, @Nullable CommandHandler> commandHandlers = new HashMap<>();
85     private List<DeviceFeatureListener> listeners = new ArrayList<>();
86     private List<DeviceFeature> connectedFeatures = new ArrayList<>();
87
88     /**
89      * Constructor
90      *
91      * @param device Insteon device to which this feature belongs
92      * @param name descriptive name for that feature
93      */
94     public DeviceFeature(InsteonDevice device, String name) {
95         this.name = name;
96         setDevice(device);
97     }
98
99     /**
100      * Constructor
101      *
102      * @param name descriptive name of the feature
103      */
104     public DeviceFeature(String name) {
105         this.name = name;
106     }
107
108     // various simple getters
109     public String getName() {
110         return name;
111     }
112
113     public synchronized QueryStatus getQueryStatus() {
114         return queryStatus;
115     }
116
117     public InsteonDevice getDevice() {
118         return device;
119     }
120
121     public boolean isFeatureGroup() {
122         return !connectedFeatures.isEmpty();
123     }
124
125     public boolean isStatusFeature() {
126         return isStatus;
127     }
128
129     public int getDirectAckTimeout() {
130         return directAckTimeout;
131     }
132
133     public MessageHandler getDefaultMsgHandler() {
134         return defaultMsgHandler;
135     }
136
137     public Map<Integer, @Nullable MessageHandler> getMsgHandlers() {
138         return this.msgHandlers;
139     }
140
141     public List<DeviceFeature> getConnectedFeatures() {
142         return (connectedFeatures);
143     }
144
145     // various simple setters
146     public void setStatusFeature(boolean f) {
147         isStatus = f;
148     }
149
150     public void setPollHandler(@Nullable PollHandler h) {
151         pollHandler = h;
152     }
153
154     public void setDevice(InsteonDevice d) {
155         device = d;
156     }
157
158     public void setMessageDispatcher(@Nullable MessageDispatcher md) {
159         dispatcher = md;
160     }
161
162     public void setDefaultCommandHandler(CommandHandler ch) {
163         defaultCommandHandler = ch;
164     }
165
166     public void setDefaultMsgHandler(MessageHandler mh) {
167         defaultMsgHandler = mh;
168     }
169
170     public synchronized void setQueryStatus(QueryStatus status) {
171         logger.trace("{} set query status to: {}", name, status);
172         queryStatus = status;
173     }
174
175     public void setTimeout(@Nullable String s) {
176         if (s != null && !s.isEmpty()) {
177             try {
178                 directAckTimeout = Integer.parseInt(s);
179                 logger.trace("ack timeout set to {}", directAckTimeout);
180             } catch (NumberFormatException e) {
181                 logger.warn("invalid number for timeout: {}", s);
182             }
183         }
184     }
185
186     /**
187      * Add a listener (item) to a device feature
188      *
189      * @param l the listener
190      */
191     public void addListener(DeviceFeatureListener l) {
192         synchronized (listeners) {
193             for (DeviceFeatureListener m : listeners) {
194                 if (m.getItemName().equals(l.getItemName())) {
195                     return;
196                 }
197             }
198             listeners.add(l);
199         }
200     }
201
202     /**
203      * Adds a connected feature such that this DeviceFeature can
204      * act as a feature group
205      *
206      * @param f the device feature related to this feature
207      */
208     public void addConnectedFeature(DeviceFeature f) {
209         connectedFeatures.add(f);
210     }
211
212     public boolean hasListeners() {
213         if (!listeners.isEmpty()) {
214             return true;
215         }
216         for (DeviceFeature f : connectedFeatures) {
217             if (f.hasListeners()) {
218                 return true;
219             }
220         }
221         return false;
222     }
223
224     /**
225      * removes a DeviceFeatureListener from this feature
226      *
227      * @param aItemName name of the item to remove as listener
228      * @return true if a listener was removed
229      */
230     public boolean removeListener(String aItemName) {
231         boolean listenerRemoved = false;
232         synchronized (listeners) {
233             for (Iterator<DeviceFeatureListener> it = listeners.iterator(); it.hasNext();) {
234                 DeviceFeatureListener fl = it.next();
235                 if (fl.getItemName().equals(aItemName)) {
236                     it.remove();
237                     listenerRemoved = true;
238                 }
239             }
240         }
241         return listenerRemoved;
242     }
243
244     public boolean isReferencedByItem(String aItemName) {
245         synchronized (listeners) {
246             for (DeviceFeatureListener fl : listeners) {
247                 if (fl.getItemName().equals(aItemName)) {
248                     return true;
249                 }
250             }
251         }
252         return false;
253     }
254
255     /**
256      * Called when message is incoming. Dispatches message according to message dispatcher
257      *
258      * @param msg The message to dispatch
259      * @return true if dispatch successful
260      */
261     public boolean handleMessage(Msg msg) {
262         MessageDispatcher dispatcher = this.dispatcher;
263         if (dispatcher == null) {
264             logger.warn("{} no dispatcher for msg {}", name, msg);
265             return false;
266         }
267         return dispatcher.dispatch(msg);
268     }
269
270     /**
271      * Called when an openhab command arrives for this device feature
272      *
273      * @param c the binding config of the item which sends the command
274      * @param cmd the command to be exectued
275      */
276     public void handleCommand(InsteonChannelConfiguration c, Command cmd) {
277         Class<? extends Command> key = cmd.getClass();
278         CommandHandler h = commandHandlers.containsKey(key) ? commandHandlers.get(key) : defaultCommandHandler;
279         if (h != null) {
280             logger.trace("{} uses {} to handle command {} for {}", getName(), h.getClass().getSimpleName(),
281                     key.getSimpleName(), getDevice().getAddress());
282             h.handleCommand(c, cmd, getDevice());
283         }
284     }
285
286     /**
287      * Make a poll message using the configured poll message handler
288      *
289      * @return the poll message
290      */
291     public @Nullable Msg makePollMsg() {
292         PollHandler pollHandler = this.pollHandler;
293         if (pollHandler == null) {
294             return null;
295         }
296         logger.trace("{} making poll msg for {} using handler {}", getName(), getDevice().getAddress(),
297                 pollHandler.getClass().getSimpleName());
298         Msg m = pollHandler.makeMsg(device);
299         return m;
300     }
301
302     /**
303      * Publish new state to all device feature listeners, but give them
304      * additional dataKey and dataValue information so they can decide
305      * whether to publish the data to the bus.
306      *
307      * @param newState state to be published
308      * @param changeType what kind of changes to publish
309      * @param dataKey the key on which to filter
310      * @param dataValue the value that must be matched
311      */
312     public void publish(State newState, StateChangeType changeType, String dataKey, String dataValue) {
313         logger.debug("{}:{} publishing: {}", this.getDevice().getAddress(), getName(), newState);
314         synchronized (listeners) {
315             for (DeviceFeatureListener listener : listeners) {
316                 listener.stateChanged(newState, changeType, dataKey, dataValue);
317             }
318         }
319     }
320
321     /**
322      * Publish new state to all device feature listeners
323      *
324      * @param newState state to be published
325      * @param changeType what kind of changes to publish
326      */
327     public void publish(State newState, StateChangeType changeType) {
328         logger.debug("{}:{} publishing: {}", this.getDevice().getAddress(), getName(), newState);
329         synchronized (listeners) {
330             for (DeviceFeatureListener listener : listeners) {
331                 listener.stateChanged(newState, changeType);
332             }
333         }
334     }
335
336     /**
337      * Poll all device feature listeners for related devices
338      */
339     public void pollRelatedDevices() {
340         synchronized (listeners) {
341             for (DeviceFeatureListener listener : listeners) {
342                 listener.pollRelatedDevices();
343             }
344         }
345     }
346
347     /**
348      * Adds a message handler to this device feature.
349      *
350      * @param cm1 The insteon cmd1 of the incoming message for which the handler should be used
351      * @param handler the handler to invoke
352      */
353     public void addMessageHandler(int cm1, @Nullable MessageHandler handler) {
354         synchronized (msgHandlers) {
355             msgHandlers.put(cm1, handler);
356         }
357     }
358
359     /**
360      * Adds a command handler to this device feature
361      *
362      * @param c the command for which this handler is invoked
363      * @param handler the handler to call
364      */
365     public void addCommandHandler(Class<? extends Command> c, @Nullable CommandHandler handler) {
366         synchronized (commandHandlers) {
367             commandHandlers.put(c, handler);
368         }
369     }
370
371     /**
372      * Turn DeviceFeature into String
373      */
374     @Override
375     public String toString() {
376         return name + "(" + listeners.size() + ":" + commandHandlers.size() + ":" + msgHandlers.size() + ")";
377     }
378
379     /**
380      * Factory method for creating DeviceFeatures.
381      *
382      * @param s The name of the device feature to create.
383      * @return The newly created DeviceFeature, or null if requested DeviceFeature does not exist.
384      */
385     @Nullable
386     public static DeviceFeature makeDeviceFeature(String s) {
387         DeviceFeature f = null;
388         synchronized (features) {
389             FeatureTemplate ft = features.get(s);
390             if (ft != null) {
391                 f = ft.build();
392             } else {
393                 logger.warn("unimplemented feature requested: {}", s);
394             }
395         }
396         return f;
397     }
398
399     /**
400      * Reads the features templates from an input stream and puts them in global map
401      *
402      * @param input the input stream from which to read the feature templates
403      */
404     public static void readFeatureTemplates(InputStream input) {
405         try {
406             List<FeatureTemplate> featureTemplates = FeatureTemplateLoader.readTemplates(input);
407             synchronized (features) {
408                 for (FeatureTemplate f : featureTemplates) {
409                     features.put(f.getName(), f);
410                 }
411             }
412         } catch (IOException e) {
413             logger.warn("IOException while reading device features", e);
414         } catch (ParsingException e) {
415             logger.warn("Parsing exception while reading device features", e);
416         }
417     }
418
419     /**
420      * Reads the feature templates from a file and adds them to a global map
421      *
422      * @param file name of the file to read from
423      */
424     public static void readFeatureTemplates(String file) {
425         try {
426             FileInputStream fis = new FileInputStream(file);
427             readFeatureTemplates(fis);
428         } catch (FileNotFoundException e) {
429             logger.warn("cannot read feature templates from file {} ", file, e);
430         }
431     }
432
433     /**
434      * static initializer
435      */
436     static {
437         // read features from xml file and store them in a map
438         InputStream input = DeviceFeature.class.getResourceAsStream("/device_features.xml");
439         Objects.requireNonNull(input);
440         readFeatureTemplates(input);
441     }
442 }