2 * Copyright (c) 2010-2024 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.insteon.internal.device;
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;
24 import java.util.Objects;
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.openhab.binding.insteon.internal.config.InsteonChannelConfiguration;
29 import org.openhab.binding.insteon.internal.device.DeviceFeatureListener.StateChangeType;
30 import org.openhab.binding.insteon.internal.message.Msg;
31 import org.openhab.binding.insteon.internal.utils.Utils.ParsingException;
32 import org.openhab.core.types.Command;
33 import org.openhab.core.types.State;
34 import org.slf4j.Logger;
35 import org.slf4j.LoggerFactory;
38 * A DeviceFeature represents a certain feature (trait) of a given Insteon device, e.g. something
39 * operating under a given InsteonAddress that can be manipulated (relay) or read (sensor).
41 * The DeviceFeature does the processing of incoming messages, and handles commands for the
42 * particular feature it represents.
44 * It uses four mechanisms for that:
46 * 1) MessageDispatcher: makes high level decisions about an incoming message and then runs the
47 * 2) MessageHandler: further processes the message, updates state etc
48 * 3) CommandHandler: translates commands from the openhab bus into an Insteon message.
49 * 4) PollHandler: creates an Insteon message to query the DeviceFeature
51 * Lastly, DeviceFeatureListeners can register with the DeviceFeature to get notifications when
52 * the state of a feature has changed. In practice, a DeviceFeatureListener corresponds to an
55 * The character of a DeviceFeature is thus given by a set of message and command handlers.
56 * A FeatureTemplate captures exactly that: it says what set of handlers make up a DeviceFeature.
58 * DeviceFeatures are added to a new device by referencing a FeatureTemplate (defined in device_features.xml)
59 * from the Device definition file (device_types.xml).
61 * @author Daniel Pfrommer - Initial contribution
62 * @author Bernd Pfrommer - openHAB 1 insteonplm binding
63 * @author Rob Nielsen - Port to openHAB 2 insteon binding
66 public class DeviceFeature {
67 public enum QueryStatus {
73 private static final Logger logger = LoggerFactory.getLogger(DeviceFeature.class);
75 private static Map<String, FeatureTemplate> features = new HashMap<>();
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;
83 private MessageHandler defaultMsgHandler = new MessageHandler.DefaultMsgHandler(this);
84 private CommandHandler defaultCommandHandler = new CommandHandler.WarnCommandHandler(this);
85 private @Nullable PollHandler pollHandler = null;
86 private @Nullable MessageDispatcher dispatcher = null;
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<>();
96 * @param device Insteon device to which this feature belongs
97 * @param name descriptive name for that feature
99 public DeviceFeature(InsteonDevice device, String name) {
107 * @param name descriptive name of the feature
109 public DeviceFeature(String name) {
113 // various simple getters
114 public String getName() {
118 public synchronized QueryStatus getQueryStatus() {
122 public InsteonDevice getDevice() {
126 public boolean isFeatureGroup() {
127 return !connectedFeatures.isEmpty();
130 public boolean isStatusFeature() {
134 public int getDirectAckTimeout() {
135 return directAckTimeout;
138 public MessageHandler getDefaultMsgHandler() {
139 return defaultMsgHandler;
142 public Map<Integer, @Nullable MessageHandler> getMsgHandlers() {
143 return this.msgHandlers;
146 public List<DeviceFeature> getConnectedFeatures() {
147 return (connectedFeatures);
150 // various simple setters
151 public void setStatusFeature(boolean f) {
155 public void setPollHandler(@Nullable PollHandler h) {
159 public void setDevice(InsteonDevice d) {
163 public void setMessageDispatcher(@Nullable MessageDispatcher md) {
167 public void setDefaultCommandHandler(CommandHandler ch) {
168 defaultCommandHandler = ch;
171 public void setDefaultMsgHandler(MessageHandler mh) {
172 defaultMsgHandler = mh;
175 public synchronized void setQueryStatus(QueryStatus status) {
176 logger.trace("{} set query status to: {}", name, status);
177 queryStatus = status;
180 public void setTimeout(@Nullable String s) {
181 if (s != null && !s.isEmpty()) {
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);
192 * Add a listener (item) to a device feature
194 * @param l the listener
196 public void addListener(DeviceFeatureListener l) {
197 synchronized (listeners) {
198 for (DeviceFeatureListener m : listeners) {
199 if (m.getItemName().equals(l.getItemName())) {
208 * Adds a connected feature such that this DeviceFeature can
209 * act as a feature group
211 * @param f the device feature related to this feature
213 public void addConnectedFeature(DeviceFeature f) {
214 connectedFeatures.add(f);
217 public boolean hasListeners() {
218 if (!listeners.isEmpty()) {
221 for (DeviceFeature f : connectedFeatures) {
222 if (f.hasListeners()) {
230 * removes a DeviceFeatureListener from this feature
232 * @param aItemName name of the item to remove as listener
233 * @return true if a listener was removed
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)) {
242 listenerRemoved = true;
246 return listenerRemoved;
249 public boolean isReferencedByItem(String aItemName) {
250 synchronized (listeners) {
251 for (DeviceFeatureListener fl : listeners) {
252 if (fl.getItemName().equals(aItemName)) {
261 * Called when message is incoming. Dispatches message according to message dispatcher
263 * @param msg The message to dispatch
264 * @return true if dispatch successful
266 public boolean handleMessage(Msg msg) {
267 MessageDispatcher dispatcher = this.dispatcher;
268 if (dispatcher == null) {
269 logger.warn("{} no dispatcher for msg {}", name, msg);
272 return dispatcher.dispatch(msg);
276 * Called when an openhab command arrives for this device feature
278 * @param c the binding config of the item which sends the command
279 * @param cmd the command to be exectued
281 public void handleCommand(InsteonChannelConfiguration c, Command cmd) {
282 Class<? extends Command> key = cmd.getClass();
283 CommandHandler h = commandHandlers.containsKey(key) ? commandHandlers.get(key) : defaultCommandHandler;
285 logger.trace("{} uses {} to handle command {} for {}", getName(), h.getClass().getSimpleName(),
286 key.getSimpleName(), getDevice().getAddress());
287 h.handleCommand(c, cmd, getDevice());
292 * Make a poll message using the configured poll message handler
294 * @return the poll message
296 public @Nullable Msg makePollMsg() {
297 PollHandler pollHandler = this.pollHandler;
298 if (pollHandler == null) {
301 logger.trace("{} making poll msg for {} using handler {}", getName(), getDevice().getAddress(),
302 pollHandler.getClass().getSimpleName());
303 return pollHandler.makeMsg(device);
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.
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
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);
326 * Publish new state to all device feature listeners
328 * @param newState state to be published
329 * @param changeType what kind of changes to publish
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);
341 * Poll all device feature listeners for related devices
343 public void pollRelatedDevices() {
344 synchronized (listeners) {
345 for (DeviceFeatureListener listener : listeners) {
346 listener.pollRelatedDevices();
352 * Adds a message handler to this device feature.
354 * @param cm1 The insteon cmd1 of the incoming message for which the handler should be used
355 * @param handler the handler to invoke
357 public void addMessageHandler(int cm1, @Nullable MessageHandler handler) {
358 synchronized (msgHandlers) {
359 msgHandlers.put(cm1, handler);
364 * Adds a command handler to this device feature
366 * @param c the command for which this handler is invoked
367 * @param handler the handler to call
369 public void addCommandHandler(Class<? extends Command> c, @Nullable CommandHandler handler) {
370 synchronized (commandHandlers) {
371 commandHandlers.put(c, handler);
376 * Turn DeviceFeature into String
379 public String toString() {
380 return name + "(" + listeners.size() + ":" + commandHandlers.size() + ":" + msgHandlers.size() + ")";
384 * Factory method for creating DeviceFeatures.
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.
390 public static DeviceFeature makeDeviceFeature(String s) {
391 DeviceFeature f = null;
392 synchronized (features) {
393 FeatureTemplate ft = features.get(s);
397 logger.warn("unimplemented feature requested: {}", s);
404 * Reads the features templates from an input stream and puts them in global map
406 * @param input the input stream from which to read the feature templates
408 public static void readFeatureTemplates(InputStream input) {
410 List<FeatureTemplate> featureTemplates = FeatureTemplateLoader.readTemplates(input);
411 synchronized (features) {
412 for (FeatureTemplate f : featureTemplates) {
413 features.put(f.getName(), f);
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);
424 * Reads the feature templates from a file and adds them to a global map
426 * @param file name of the file to read from
428 public static void readFeatureTemplates(String file) {
430 FileInputStream fis = new FileInputStream(file);
431 readFeatureTemplates(fis);
432 } catch (FileNotFoundException e) {
433 logger.warn("cannot read feature templates from file {} ", file, e);
441 // read features from xml file and store them in a map
442 InputStream input = DeviceFeature.class.getResourceAsStream("/device_features.xml");
443 Objects.requireNonNull(input);
444 readFeatureTemplates(input);