]> git.basschouten.com Git - openhab-addons.git/blob
445b2c9a824ec1707145944660c4c046fcf7d7fe
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 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.transport.message;
14
15 import java.util.LinkedHashMap;
16 import java.util.Map;
17 import java.util.Map.Entry;
18
19 import org.eclipse.jdt.annotation.NonNullByDefault;
20 import org.eclipse.jdt.annotation.Nullable;
21 import org.openhab.binding.insteon.internal.InsteonResourceLoader;
22 import org.openhab.binding.insteon.internal.device.InsteonAddress;
23 import org.openhab.binding.insteon.internal.transport.message.Msg.Direction;
24 import org.openhab.binding.insteon.internal.utils.HexUtils;
25 import org.w3c.dom.Element;
26 import org.w3c.dom.Node;
27 import org.w3c.dom.NodeList;
28 import org.xml.sax.SAXException;
29
30 /**
31  * The {@link MsgDefinitionRegistry} represents the message definition registry
32  *
33  * @author Bernd Pfrommer - Initial contribution
34  * @author Rob Nielsen - Port to openHAB 2 insteon binding
35  * @author Jeremy Setton - Rewrite insteon binding
36  */
37 @NonNullByDefault
38 public class MsgDefinitionRegistry extends InsteonResourceLoader {
39     private static final MsgDefinitionRegistry MSG_DEFINITION_REGISTRY = new MsgDefinitionRegistry();
40     private static final String RESOURCE_NAME = "/msg-definitions.xml";
41
42     private Map<String, Msg> definitions = new LinkedHashMap<>();
43
44     private MsgDefinitionRegistry() {
45         super(RESOURCE_NAME);
46     }
47
48     /**
49      * Returns message template for a given type
50      *
51      * @param type message type to match
52      * @return message template if found, otherwise null
53      */
54     public @Nullable Msg getTemplate(String type) {
55         return definitions.get(type);
56     }
57
58     /**
59      * Returns message template for a given command and direction
60      *
61      * @param cmd message command to match
62      * @param direction message direction to match
63      * @return message template if found, otherwise null
64      */
65     public @Nullable Msg getTemplate(byte cmd, Direction direction) {
66         return getTemplate(cmd, null, direction);
67     }
68
69     /**
70      * Returns message template for a given command, extended flag and direction
71      *
72      * @param cmd message command to match
73      * @param isExtended if message is extended
74      * @param direction message direction to match
75      * @return message template if found, otherwise null
76      */
77     public @Nullable Msg getTemplate(byte cmd, @Nullable Boolean isExtended, Direction direction) {
78         return definitions.values().stream().filter(msg -> msg.getCommand() == cmd && msg.getDirection() == direction
79                 && (isExtended == null || msg.isExtended() == isExtended)).findFirst().orElse(null);
80     }
81
82     /**
83      * Returns known message definitions
84      *
85      * @return currently known message definitions
86      */
87     public Map<String, Msg> getDefinitions() {
88         return definitions;
89     }
90
91     /**
92      * Initializes message definition registry
93      */
94     @Override
95     protected void initialize() {
96         super.initialize();
97
98         logger.debug("loaded {} message definitions", definitions.size());
99         if (logger.isTraceEnabled()) {
100             definitions.entrySet().stream()
101                     .map(definition -> String.format("%s->%s", definition.getKey(), definition.getValue()))
102                     .forEach(logger::trace);
103         }
104     }
105
106     /**
107      * Parses message definition document
108      *
109      * @param element element to parse
110      * @throws SAXException
111      */
112     @Override
113     protected void parseDocument(Element element) throws SAXException {
114         NodeList nodes = element.getChildNodes();
115         for (int i = 0; i < nodes.getLength(); i++) {
116             Node node = nodes.item(i);
117             if (node.getNodeType() == Node.ELEMENT_NODE) {
118                 Element child = (Element) node;
119                 String nodeName = child.getNodeName();
120                 if ("msg".equals(nodeName)) {
121                     parseMsgDefinition(child);
122                 }
123             }
124         }
125     }
126
127     /**
128      * Parses message definition node
129      *
130      * @param element element to parse
131      * @throws SAXException
132      */
133     private void parseMsgDefinition(Element element) throws SAXException {
134         LinkedHashMap<Field, Object> fields = new LinkedHashMap<>();
135         String name = element.getAttribute("name");
136         Direction direction = Direction.valueOf(element.getAttribute("direction"));
137         int length = element.hasAttribute("length") ? Integer.parseInt(element.getAttribute("length")) : 0;
138         int headerLength = 0;
139         int offset = 0;
140
141         NodeList nodes = element.getChildNodes();
142         for (int i = 0; i < nodes.getLength(); i++) {
143             Node node = nodes.item(i);
144             if (node.getNodeType() == Node.ELEMENT_NODE) {
145                 Element child = (Element) node;
146                 String nodeName = child.getNodeName();
147                 if ("header".equals(nodeName)) {
148                     headerLength = parseHeader(child, fields);
149                     // Increment the offset by the header length
150                     offset += headerLength;
151                 } else {
152                     // Increment the offset by the field data type length
153                     offset += parseField(child, offset, fields);
154                 }
155             }
156         }
157         if (length == 0) {
158             length = offset;
159         } else if (offset != length) {
160             throw new SAXException("actual msg length " + offset + " differs from given msg length " + length);
161         }
162
163         try {
164             Msg msg = makeMsgTemplate(fields, headerLength, length, direction);
165             definitions.put(name, msg);
166         } catch (FieldException e) {
167             throw new SAXException("failed to create message definition " + name + ":", e);
168         }
169     }
170
171     /**
172      * Parses header node
173      *
174      * @param element element to parse
175      * @param fields fields map to update
176      * @return header length
177      * @throws SAXException
178      */
179     private int parseHeader(Element element, LinkedHashMap<Field, Object> fields) throws SAXException {
180         int length = Integer.parseInt(element.getAttribute("length"));
181         int offset = 0;
182
183         NodeList nodes = element.getChildNodes();
184         for (int i = 0; i < nodes.getLength(); i++) {
185             Node node = nodes.item(i);
186             if (node.getNodeType() == Node.ELEMENT_NODE) {
187                 Element child = (Element) node;
188                 // Increment the offset by the field data type length
189                 offset += parseField(child, offset, fields);
190             }
191         }
192         if (length != offset) {
193             throw new SAXException("actual header length " + offset + " differs from given length " + length);
194         }
195         return length;
196     }
197
198     /**
199      * Parses field node
200      *
201      * @param element element to parse
202      * @param offset msg offset
203      * @param fields fields map to update
204      * @return field data type length
205      * @throws SAXException
206      */
207     private int parseField(Element element, int offset, LinkedHashMap<Field, Object> fields) throws SAXException {
208         String name = element.getAttribute("name");
209         if (name == null) {
210             throw new SAXException("undefined field name");
211         }
212         DataType dataType = DataType.get(element.getNodeName());
213         Field field = new Field(name, dataType, offset);
214         Object value = getFieldValue(dataType, element.getTextContent().trim());
215         fields.put(field, value);
216         return dataType.getSize();
217     }
218
219     /**
220      * Returns field value
221      *
222      * @param dataType field data type
223      * @param value value to convert
224      * @return field value
225      * @throws SAXException
226      */
227     private Object getFieldValue(DataType dataType, String value) throws SAXException {
228         switch (dataType) {
229             case BYTE:
230                 return getByteValue(value);
231             case ADDRESS:
232                 return getAddressValue(value);
233             default:
234                 throw new SAXException("invalid field data type");
235         }
236     }
237
238     /**
239      * Returns field value as a byte
240      *
241      * @param value value to convert
242      * @return byte
243      * @throws SAXException
244      */
245     private byte getByteValue(String value) throws SAXException {
246         try {
247             return value.isEmpty() ? 0x00 : (byte) HexUtils.toInteger(value);
248         } catch (NumberFormatException e) {
249             throw new SAXException("invalid field byte value: " + value);
250         }
251     }
252
253     /**
254      * Returns field value as an insteon address
255      *
256      * @param value value to convert
257      * @return insteon address
258      * @throws SAXException
259      */
260     private InsteonAddress getAddressValue(String value) throws SAXException {
261         try {
262             return value.isEmpty() ? InsteonAddress.UNKNOWN : new InsteonAddress(value);
263         } catch (IllegalArgumentException e) {
264             throw new SAXException("invalid field address value: " + value);
265         }
266     }
267
268     /**
269      * Returns new message template
270      *
271      * @param fields msg fields
272      * @param length msg length
273      * @param headerLength header length
274      * @param direction msg direction
275      * @return new msg template
276      * @throws FieldException
277      */
278     private Msg makeMsgTemplate(Map<Field, Object> fields, int headerLength, int length, Direction direction)
279             throws FieldException {
280         Msg msg = new Msg(headerLength, length, direction);
281         for (Entry<Field, Object> entry : fields.entrySet()) {
282             Field field = entry.getKey();
283             byte[] data = msg.getData();
284             field.set(data, entry.getValue());
285             if (!field.getName().isEmpty()) {
286                 msg.addField(field);
287             }
288         }
289         return msg;
290     }
291
292     /**
293      * Singleton instance function
294      *
295      * @return MsgDefinitionRegistry singleton reference
296      */
297     public static synchronized MsgDefinitionRegistry getInstance() {
298         if (MSG_DEFINITION_REGISTRY.getDefinitions().isEmpty()) {
299             MSG_DEFINITION_REGISTRY.initialize();
300         }
301         return MSG_DEFINITION_REGISTRY;
302     }
303 }