]> git.basschouten.com Git - openhab-addons.git/blob
0d1f20e9aceb3a0c7a308a57cbfdcc54caa13ef8
[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 @SuppressWarnings("null")
66 public class DeviceFeature {
67     public static enum QueryStatus {
68         NEVER_QUERIED,
69         QUERY_PENDING,
70         QUERY_ANSWERED
71     }
72
73     private static final Logger logger = LoggerFactory.getLogger(DeviceFeature.class);
74
75     private static Map<String, FeatureTemplate> features = new HashMap<>();
76
77     private InsteonDevice device = new InsteonDevice();
78     private String name = "INVALID_FEATURE_NAME";
79     private boolean isStatus = false;
80     private int directAckTimeout = 6000;
81     private QueryStatus queryStatus = QueryStatus.NEVER_QUERIED;
82
83     private @Nullable MessageHandler defaultMsgHandler = new MessageHandler.DefaultMsgHandler(this);
84     private @Nullable CommandHandler defaultCommandHandler = new CommandHandler.WarnCommandHandler(this);
85     private @Nullable PollHandler pollHandler = null;
86     private @Nullable MessageDispatcher dispatcher = null;
87
88     private Map<Integer, @Nullable MessageHandler> msgHandlers = new HashMap<>();
89     private Map<Class<? extends Command>, @Nullable CommandHandler> commandHandlers = new HashMap<>();
90     private List<DeviceFeatureListener> listeners = new ArrayList<>();
91     private List<DeviceFeature> connectedFeatures = new ArrayList<>();
92
93     /**
94      * Constructor
95      *
96      * @param device Insteon device to which this feature belongs
97      * @param name descriptive name for that feature
98      */
99     public DeviceFeature(InsteonDevice device, String name) {
100         this.name = name;
101         setDevice(device);
102     }
103
104     /**
105      * Constructor
106      *
107      * @param name descriptive name of the feature
108      */
109     public DeviceFeature(String name) {
110         this.name = name;
111     }
112
113     // various simple getters
114     public String getName() {
115         return name;
116     }
117
118     public synchronized QueryStatus getQueryStatus() {
119         return queryStatus;
120     }
121
122     public InsteonDevice getDevice() {
123         return device;
124     }
125
126     public boolean isFeatureGroup() {
127         return !connectedFeatures.isEmpty();
128     }
129
130     public boolean isStatusFeature() {
131         return isStatus;
132     }
133
134     public int getDirectAckTimeout() {
135         return directAckTimeout;
136     }
137
138     public @Nullable MessageHandler getDefaultMsgHandler() {
139         return defaultMsgHandler;
140     }
141
142     public Map<Integer, @Nullable MessageHandler> getMsgHandlers() {
143         return this.msgHandlers;
144     }
145
146     public List<DeviceFeature> getConnectedFeatures() {
147         return (connectedFeatures);
148     }
149
150     // various simple setters
151     public void setStatusFeature(boolean f) {
152         isStatus = f;
153     }
154
155     public void setPollHandler(@Nullable PollHandler h) {
156         pollHandler = h;
157     }
158
159     public void setDevice(InsteonDevice d) {
160         device = d;
161     }
162
163     public void setMessageDispatcher(@Nullable MessageDispatcher md) {
164         dispatcher = md;
165     }
166
167     public void setDefaultCommandHandler(@Nullable CommandHandler ch) {
168         defaultCommandHandler = ch;
169     }
170
171     public void setDefaultMsgHandler(@Nullable MessageHandler mh) {
172         defaultMsgHandler = mh;
173     }
174
175     public synchronized void setQueryStatus(QueryStatus status) {
176         logger.trace("{} set query status to: {}", name, status);
177         queryStatus = status;
178     }
179
180     public void setTimeout(@Nullable String s) {
181         if (s != null && !s.isEmpty()) {
182             try {
183                 directAckTimeout = Integer.parseInt(s);
184                 logger.trace("ack timeout set to {}", directAckTimeout);
185             } catch (NumberFormatException e) {
186                 logger.warn("invalid number for timeout: {}", s);
187             }
188         }
189     }
190
191     /**
192      * Add a listener (item) to a device feature
193      *
194      * @param l the listener
195      */
196     public void addListener(DeviceFeatureListener l) {
197         synchronized (listeners) {
198             for (DeviceFeatureListener m : listeners) {
199                 if (m.getItemName().equals(l.getItemName())) {
200                     return;
201                 }
202             }
203             listeners.add(l);
204         }
205     }
206
207     /**
208      * Adds a connected feature such that this DeviceFeature can
209      * act as a feature group
210      *
211      * @param f the device feature related to this feature
212      */
213     public void addConnectedFeature(DeviceFeature f) {
214         connectedFeatures.add(f);
215     }
216
217     public boolean hasListeners() {
218         if (!listeners.isEmpty()) {
219             return true;
220         }
221         for (DeviceFeature f : connectedFeatures) {
222             if (f.hasListeners()) {
223                 return true;
224             }
225         }
226         return false;
227     }
228
229     /**
230      * removes a DeviceFeatureListener from this feature
231      *
232      * @param aItemName name of the item to remove as listener
233      * @return true if a listener was removed
234      */
235     public boolean removeListener(String aItemName) {
236         boolean listenerRemoved = false;
237         synchronized (listeners) {
238             for (Iterator<DeviceFeatureListener> it = listeners.iterator(); it.hasNext();) {
239                 DeviceFeatureListener fl = it.next();
240                 if (fl.getItemName().equals(aItemName)) {
241                     it.remove();
242                     listenerRemoved = true;
243                 }
244             }
245         }
246         return listenerRemoved;
247     }
248
249     public boolean isReferencedByItem(String aItemName) {
250         synchronized (listeners) {
251             for (DeviceFeatureListener fl : listeners) {
252                 if (fl.getItemName().equals(aItemName)) {
253                     return true;
254                 }
255             }
256         }
257         return false;
258     }
259
260     /**
261      * Called when message is incoming. Dispatches message according to message dispatcher
262      *
263      * @param msg The message to dispatch
264      * @return true if dispatch successful
265      */
266     public boolean handleMessage(Msg msg) {
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         logger.trace("{} uses {} to handle command {} for {}", getName(), h.getClass().getSimpleName(),
284                 key.getSimpleName(), getDevice().getAddress());
285         h.handleCommand(c, cmd, getDevice());
286     }
287
288     /**
289      * Make a poll message using the configured poll message handler
290      *
291      * @return the poll message
292      */
293     public @Nullable Msg makePollMsg() {
294         if (pollHandler == null) {
295             return null;
296         }
297         logger.trace("{} making poll msg for {} using handler {}", getName(), getDevice().getAddress(),
298                 pollHandler.getClass().getSimpleName());
299         Msg m = pollHandler.makeMsg(device);
300         return m;
301     }
302
303     /**
304      * Publish new state to all device feature listeners, but give them
305      * additional dataKey and dataValue information so they can decide
306      * whether to publish the data to the bus.
307      *
308      * @param newState state to be published
309      * @param changeType what kind of changes to publish
310      * @param dataKey the key on which to filter
311      * @param dataValue the value that must be matched
312      */
313     public void publish(State newState, StateChangeType changeType, String dataKey, String dataValue) {
314         logger.debug("{}:{} publishing: {}", this.getDevice().getAddress(), getName(), newState);
315         synchronized (listeners) {
316             for (DeviceFeatureListener listener : listeners) {
317                 listener.stateChanged(newState, changeType, dataKey, dataValue);
318             }
319         }
320     }
321
322     /**
323      * Publish new state to all device feature listeners
324      *
325      * @param newState state to be published
326      * @param changeType what kind of changes to publish
327      */
328     public void publish(State newState, StateChangeType changeType) {
329         logger.debug("{}:{} publishing: {}", this.getDevice().getAddress(), getName(), newState);
330         synchronized (listeners) {
331             for (DeviceFeatureListener listener : listeners) {
332                 listener.stateChanged(newState, changeType);
333             }
334         }
335     }
336
337     /**
338      * Poll all device feature listeners for related devices
339      */
340     public void pollRelatedDevices() {
341         synchronized (listeners) {
342             for (DeviceFeatureListener listener : listeners) {
343                 listener.pollRelatedDevices();
344             }
345         }
346     }
347
348     /**
349      * Adds a message handler to this device feature.
350      *
351      * @param cm1 The insteon cmd1 of the incoming message for which the handler should be used
352      * @param handler the handler to invoke
353      */
354     public void addMessageHandler(int cm1, @Nullable MessageHandler handler) {
355         synchronized (msgHandlers) {
356             msgHandlers.put(cm1, handler);
357         }
358     }
359
360     /**
361      * Adds a command handler to this device feature
362      *
363      * @param c the command for which this handler is invoked
364      * @param handler the handler to call
365      */
366     public void addCommandHandler(Class<? extends Command> c, @Nullable CommandHandler handler) {
367         synchronized (commandHandlers) {
368             commandHandlers.put(c, handler);
369         }
370     }
371
372     /**
373      * Turn DeviceFeature into String
374      */
375     @Override
376     public String toString() {
377         return name + "(" + listeners.size() + ":" + commandHandlers.size() + ":" + msgHandlers.size() + ")";
378     }
379
380     /**
381      * Factory method for creating DeviceFeatures.
382      *
383      * @param s The name of the device feature to create.
384      * @return The newly created DeviceFeature, or null if requested DeviceFeature does not exist.
385      */
386     @Nullable
387     public static DeviceFeature makeDeviceFeature(String s) {
388         DeviceFeature f = null;
389         synchronized (features) {
390             if (features.containsKey(s)) {
391                 f = features.get(s).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         readFeatureTemplates(input);
440     }
441 }