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