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.database;
15 import java.io.IOException;
16 import java.util.HashSet;
18 import java.util.concurrent.ScheduledExecutorService;
19 import java.util.concurrent.ScheduledFuture;
20 import java.util.concurrent.TimeUnit;
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;
37 * The {@link ModemDBReader} manages modem database read requests
39 * @author Jeremy Setton - Initial contribution
42 public class ModemDBReader implements PortListener {
43 private final Logger logger = LoggerFactory.getLogger(ModemDBReader.class);
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;
53 public ModemDBReader(InsteonModem modem, ScheduledExecutorService scheduler) {
55 this.scheduler = scheduler;
57 modem.getPort().registerListener(this);
60 public boolean isRunning() {
65 logger.debug("starting modem database reader");
69 job = scheduler.scheduleWithFixedDelay(() -> {
70 if (System.currentTimeMillis() - lastMsgReceived > DatabaseManager.MESSAGE_TIMEOUT) {
72 if (messageCount == 0) {
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.\
78 logger.warn("Failed to read modem database, restarting!{}", s);
81 }, 0, 1, TimeUnit.SECONDS);
85 logger.debug("modem database reader finished");
87 ScheduledFuture<?> job = this.job;
93 modem.getDBM().operationCompleted();
96 private void restart() {
97 modem.getDB().clear();
102 private void getAllRecords() {
103 lastMsgReceived = System.currentTimeMillis();
106 getFirstLinkRecord();
109 private void done() {
110 modem.getDB().recordsLoaded();
115 private void getFirstLinkRecord() {
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);
126 private void getNextLinkRecord() {
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);
137 private void getProductId(InsteonAddress address) {
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);
151 public void disconnected() {
153 logger.debug("port disconnected, restarting");
159 public void messageReceived(Msg msg) {
161 lastMsgReceived = msg.getTimestamp();
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
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
188 logger.debug("got all link records");
191 } else if (msg.getCommand() == 0x6F && msg.isReplyAck()) {
192 // we got a manage link record reply ack
193 handleLinkRecordUpdated(msg);
195 } catch (FieldException e) {
196 logger.warn("error parsing modem link record field ", e);
201 public void messageSent(Msg msg) {
202 // ignore outbound message
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)) {
210 // get product id if not already queried
211 synchronized (productQueries) {
212 if (productQueries.add(address)) {
213 getProductId(address);
218 private void handleLinkRecord(Msg msg) throws FieldException {
220 logger.debug("unsolicited link record, ignoring");
223 ModemDBRecord record = ModemDBRecord.fromRecordMsg(msg);
224 InsteonAddress address = msg.getInsteonAddress("LinkAddr");
225 modem.getDB().addRecord(record);
226 getProductData(address);
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);
238 modem.getDB().modifyOrAddRecord(record);
240 case MODIFY_CONTROLLER_OR_ADD:
241 modem.getDB().modifyOrAddControllerRecord(record);
243 case MODIFY_RESPONDER_OR_ADD:
244 modem.getDB().modifyOrAddResponderRecord(record);
247 modem.getDB().deleteRecord(address, group);
250 logger.debug("got invalid control code: {}", HexUtils.getHexString(code));
253 modem.getDB().linkUpdated(address, group, false);
254 getProductData(address);
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);
265 modem.getDB().modifyOrAddControllerRecord(record);
268 modem.getDB().modifyOrAddResponderRecord(record);
271 modem.getDB().deleteRecord(address, group);
274 logger.debug("got invalid link code: {}", HexUtils.getHexString(code));
277 modem.getDB().linkUpdated(address, group, true);
278 getProductData(address);
281 private void handleAllLinkMessage(Msg msg) throws FieldException {
282 InsteonAddress address = msg.getInsteonAddress("fromAddress");
283 getProductData(address);
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);
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);
310 private void handleIMReset() {
311 modem.resetInitiated();