]> git.basschouten.com Git - openhab-addons.git/blob
e748c24cb604d26228062a6cc304a4f30be617f9
[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.device.database;
14
15 import java.io.IOException;
16 import java.util.HashSet;
17 import java.util.Set;
18 import java.util.concurrent.ScheduledExecutorService;
19 import java.util.concurrent.ScheduledFuture;
20 import java.util.concurrent.TimeUnit;
21
22 import org.eclipse.jdt.annotation.NonNullByDefault;
23 import org.eclipse.jdt.annotation.Nullable;
24 import org.openhab.binding.insteon.internal.device.InsteonAddress;
25 import org.openhab.binding.insteon.internal.device.InsteonModem;
26 import org.openhab.binding.insteon.internal.device.ProductData;
27 import org.openhab.binding.insteon.internal.device.ProductDataRegistry;
28 import org.openhab.binding.insteon.internal.transport.PortListener;
29 import org.openhab.binding.insteon.internal.transport.message.FieldException;
30 import org.openhab.binding.insteon.internal.transport.message.InvalidMessageTypeException;
31 import org.openhab.binding.insteon.internal.transport.message.Msg;
32 import org.openhab.binding.insteon.internal.utils.HexUtils;
33 import org.slf4j.Logger;
34 import org.slf4j.LoggerFactory;
35
36 /**
37  * The {@link ModemDBReader} manages modem database read requests
38  *
39  * @author Jeremy Setton - Initial contribution
40  */
41 @NonNullByDefault
42 public class ModemDBReader implements PortListener {
43     private final Logger logger = LoggerFactory.getLogger(ModemDBReader.class);
44
45     private InsteonModem modem;
46     private ScheduledExecutorService scheduler;
47     private @Nullable ScheduledFuture<?> job;
48     private Set<InsteonAddress> productQueries = new HashSet<>();
49     private boolean done = true;
50     private long lastMsgReceived;
51     private int messageCount;
52
53     public ModemDBReader(InsteonModem modem, ScheduledExecutorService scheduler) {
54         this.modem = modem;
55         this.scheduler = scheduler;
56
57         modem.getPort().registerListener(this);
58     }
59
60     public boolean isRunning() {
61         return job != null;
62     }
63
64     public void read() {
65         logger.debug("starting modem database reader");
66
67         getAllRecords();
68
69         job = scheduler.scheduleWithFixedDelay(() -> {
70             if (System.currentTimeMillis() - lastMsgReceived > DatabaseManager.MESSAGE_TIMEOUT) {
71                 String s = "";
72                 if (messageCount == 0) {
73                     s = """
74                             No messages were received, the PLM or hub might be broken. If this continues see \
75                             'Known Limitations and Issues' in the Insteon binding documentation.\
76                             """;
77                 }
78                 logger.warn("Failed to read modem database, restarting!{}", s);
79                 restart();
80             }
81         }, 0, 1, TimeUnit.SECONDS);
82     }
83
84     public void stop() {
85         logger.debug("modem database reader finished");
86
87         ScheduledFuture<?> job = this.job;
88         if (job != null) {
89             job.cancel(true);
90             this.job = null;
91         }
92
93         modem.getDBM().operationCompleted();
94     }
95
96     private void restart() {
97         modem.getDB().clear();
98         modem.reconnect();
99         getAllRecords();
100     }
101
102     private void getAllRecords() {
103         lastMsgReceived = System.currentTimeMillis();
104         messageCount = 0;
105         done = false;
106         getFirstLinkRecord();
107     }
108
109     private void done() {
110         modem.getDB().recordsLoaded();
111         done = true;
112         stop();
113     }
114
115     private void getFirstLinkRecord() {
116         try {
117             Msg msg = Msg.makeMessage("GetFirstALLLinkRecord");
118             modem.writeMessage(msg);
119         } catch (IOException e) {
120             logger.warn("error sending first link record query ", e);
121         } catch (InvalidMessageTypeException e) {
122             logger.warn("invalid message ", e);
123         }
124     }
125
126     private void getNextLinkRecord() {
127         try {
128             Msg msg = Msg.makeMessage("GetNextALLLinkRecord");
129             modem.writeMessage(msg);
130         } catch (IOException e) {
131             logger.warn("error sending next link record query ", e);
132         } catch (InvalidMessageTypeException e) {
133             logger.warn("invalid message ", e);
134         }
135     }
136
137     private void getProductId(InsteonAddress address) {
138         try {
139             Msg msg = Msg.makeStandardMessage(address, (byte) 0x10, (byte) 0x00);
140             modem.writeMessage(msg);
141         } catch (FieldException e) {
142             logger.warn("cannot access field:", e);
143         } catch (IOException e) {
144             logger.warn("error sending product id query ", e);
145         } catch (InvalidMessageTypeException e) {
146             logger.warn("invalid message ", e);
147         }
148     }
149
150     @Override
151     public void disconnected() {
152         if (!done) {
153             logger.debug("port disconnected, restarting");
154             restart();
155         }
156     }
157
158     @Override
159     public void messageReceived(Msg msg) {
160         if (isRunning()) {
161             lastMsgReceived = msg.getTimestamp();
162             messageCount++;
163         }
164
165         try {
166             if (msg.getCommand() == 0x50 && (msg.isAllLinkCleanup() || msg.isAllLinkSuccessReport())) {
167                 // we got an all link cleanup or success report message
168                 handleAllLinkMessage(msg);
169             } else if (msg.getCommand() == 0x50 && msg.isBroadcast()
170                     && (msg.getByte("command1") == 0x01 || msg.getByte("command1") == 0x02)) {
171                 // we got a product data broadcast message
172                 handleProductData(msg);
173             } else if ((msg.getCommand() == 0x50 || msg.getCommand() == 0x5C) && msg.getByte("command1") == 0x10) {
174                 // we got a product data request ack
175                 handleProductDataAck(msg);
176             } else if (msg.getCommand() == 0x53) {
177                 // we got a linking completed message
178                 handleLinkingCompleted(msg);
179             } else if (msg.getCommand() == 0x55 || msg.getCommand() == 0x67 && msg.isReplyAck()) {
180                 // we got a user reset detected message or im reset reply ack
181                 handleIMReset();
182             } else if (msg.getCommand() == 0x57) {
183                 // we got a link record response
184                 handleLinkRecord(msg);
185             } else if ((msg.getCommand() == 0x69 || msg.getCommand() == 0x6A) && msg.isReplyNack()) {
186                 // we got a get link record reply nack
187                 if (!done) {
188                     logger.debug("got all link records");
189                     done();
190                 }
191             } else if (msg.getCommand() == 0x6F && msg.isReplyAck()) {
192                 // we got a manage link record reply ack
193                 handleLinkRecordUpdated(msg);
194             }
195         } catch (FieldException e) {
196             logger.warn("error parsing modem link record field ", e);
197         }
198     }
199
200     @Override
201     public void messageSent(Msg msg) {
202         // ignore outbound message
203     }
204
205     private void getProductData(InsteonAddress address) {
206         // skip if not in modem db or product data already known
207         if (!modem.getDB().hasEntry(address) || modem.getDB().hasProductData(address)) {
208             return;
209         }
210         // get product id if not already queried
211         synchronized (productQueries) {
212             if (productQueries.add(address)) {
213                 getProductId(address);
214             }
215         }
216     }
217
218     private void handleLinkRecord(Msg msg) throws FieldException {
219         if (done) {
220             logger.debug("unsolicited link record, ignoring");
221             return;
222         }
223         ModemDBRecord record = ModemDBRecord.fromRecordMsg(msg);
224         InsteonAddress address = msg.getInsteonAddress("LinkAddr");
225         modem.getDB().addRecord(record);
226         getProductData(address);
227         getNextLinkRecord();
228     }
229
230     private void handleLinkRecordUpdated(Msg msg) throws FieldException {
231         ModemDBRecord record = ModemDBRecord.fromRecordMsg(msg);
232         InsteonAddress address = msg.getInsteonAddress("LinkAddr");
233         int group = msg.getInt("ALLLinkGroup");
234         int code = msg.getInt("ControlCode");
235         ManageRecordAction action = ManageRecordAction.valueOf(code);
236         switch (action) {
237             case MODIFY_OR_ADD:
238                 modem.getDB().modifyOrAddRecord(record);
239                 break;
240             case MODIFY_CONTROLLER_OR_ADD:
241                 modem.getDB().modifyOrAddControllerRecord(record);
242                 break;
243             case MODIFY_RESPONDER_OR_ADD:
244                 modem.getDB().modifyOrAddResponderRecord(record);
245                 break;
246             case DELETE:
247                 modem.getDB().deleteRecord(address, group);
248                 break;
249             default:
250                 logger.debug("got invalid control code: {}", HexUtils.getHexString(code));
251                 return;
252         }
253         modem.getDB().linkUpdated(address, group, false);
254         getProductData(address);
255     }
256
257     private void handleLinkingCompleted(Msg msg) throws FieldException {
258         ModemDBRecord record = ModemDBRecord.fromLinkingMsg(msg);
259         InsteonAddress address = msg.getInsteonAddress("LinkAddr");
260         int group = msg.getInt("ALLLinkGroup");
261         int code = msg.getInt("LinkCode");
262         LinkMode mode = LinkMode.valueOf(code);
263         switch (mode) {
264             case CONTROLLER:
265                 modem.getDB().modifyOrAddControllerRecord(record);
266                 break;
267             case RESPONDER:
268                 modem.getDB().modifyOrAddResponderRecord(record);
269                 break;
270             case DELETE:
271                 modem.getDB().deleteRecord(address, group);
272                 break;
273             default:
274                 logger.debug("got invalid link code: {}", HexUtils.getHexString(code));
275                 return;
276         }
277         modem.getDB().linkUpdated(address, group, true);
278         getProductData(address);
279     }
280
281     private void handleAllLinkMessage(Msg msg) throws FieldException {
282         InsteonAddress address = msg.getInsteonAddress("fromAddress");
283         getProductData(address);
284     }
285
286     private void handleProductData(Msg msg) throws FieldException {
287         InsteonAddress fromAddr = msg.getInsteonAddress("fromAddress");
288         InsteonAddress toAddr = msg.getInsteonAddress("toAddress");
289         int deviceCategory = Byte.toUnsignedInt(toAddr.getHighByte());
290         int subCategory = Byte.toUnsignedInt(toAddr.getMiddleByte());
291         int firmware = Byte.toUnsignedInt(toAddr.getLowByte());
292         int hardware = msg.getInt("command2");
293         ProductData productData = ProductDataRegistry.getInstance().getProductData(deviceCategory, subCategory);
294         productData.setFirmwareVersion(firmware);
295         productData.setHardwareVersion(hardware);
296         // set product data if in modem db
297         if (modem.getDB().hasEntry(fromAddr)) {
298             modem.getDB().setProductData(fromAddr, productData);
299         }
300     }
301
302     private void handleProductDataAck(Msg msg) throws FieldException {
303         InsteonAddress address = msg.getInsteonAddress("fromAddress");
304         // remove address from product queries
305         synchronized (productQueries) {
306             productQueries.remove(address);
307         }
308     }
309
310     private void handleIMReset() {
311         modem.resetInitiated();
312     }
313 }