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.IOException;
16 import java.util.List;
18 import java.util.Objects;
19 import java.util.concurrent.ConcurrentHashMap;
20 import java.util.concurrent.ScheduledExecutorService;
22 import org.eclipse.jdt.annotation.NonNullByDefault;
23 import org.eclipse.jdt.annotation.Nullable;
24 import org.openhab.binding.insteon.internal.config.InsteonBridgeConfiguration;
25 import org.openhab.binding.insteon.internal.device.database.DatabaseManager;
26 import org.openhab.binding.insteon.internal.device.database.ModemDB;
27 import org.openhab.binding.insteon.internal.handler.InsteonBridgeHandler;
28 import org.openhab.binding.insteon.internal.transport.Port;
29 import org.openhab.binding.insteon.internal.transport.PortListener;
30 import org.openhab.binding.insteon.internal.transport.message.FieldException;
31 import org.openhab.binding.insteon.internal.transport.message.InvalidMessageTypeException;
32 import org.openhab.binding.insteon.internal.transport.message.Msg;
33 import org.openhab.core.io.transport.serial.SerialPortManager;
36 * The {@link InsteonModem} represents an Insteom modem
38 * @author Jeremy Setton - Initial contribution
41 public class InsteonModem extends BaseDevice<InsteonAddress, InsteonBridgeHandler> implements PortListener {
42 private static final int RESET_TIME = 20; // in seconds
45 private ModemDB modemDB;
46 private DatabaseManager dbm;
47 private LinkManager linker;
48 private PollManager poller;
49 private RequestManager requester;
50 private Map<DeviceAddress, Device> devices = new ConcurrentHashMap<>();
51 private Map<Integer, Scene> scenes = new ConcurrentHashMap<>();
52 private @Nullable X10Address lastX10Address;
53 private boolean initialized = false;
54 private int msgsReceived = 0;
56 public InsteonModem(InsteonBridgeConfiguration config, ScheduledExecutorService scheduler,
57 SerialPortManager serialPortManager) {
58 super(InsteonAddress.UNKNOWN);
59 this.port = new Port(config, scheduler, serialPortManager);
60 this.modemDB = new ModemDB(this);
61 this.dbm = new DatabaseManager(this, scheduler);
62 this.linker = new LinkManager(this, scheduler);
63 this.poller = new PollManager(scheduler);
64 this.requester = new RequestManager(scheduler);
68 public @Nullable InsteonModem getModem() {
72 public Port getPort() {
76 public ModemDB getDB() {
80 public DatabaseManager getDBM() {
84 public LinkManager getLinkManager() {
88 public PollManager getPollManager() {
92 public RequestManager getRequestManager() {
96 public @Nullable Device getDevice(DeviceAddress address) {
97 return devices.get(address);
100 public boolean hasDevice(DeviceAddress address) {
101 return devices.containsKey(address);
104 public List<Device> getDevices() {
105 return devices.values().stream().toList();
108 public @Nullable InsteonDevice getInsteonDevice(InsteonAddress address) {
109 return (InsteonDevice) getDevice(address);
112 public List<InsteonDevice> getInsteonDevices() {
113 return getDevices().stream().filter(InsteonDevice.class::isInstance).map(InsteonDevice.class::cast).toList();
116 public @Nullable X10Device getX10Device(X10Address address) {
117 return (X10Device) getDevice(address);
120 public List<X10Device> getX10Devices() {
121 return getDevices().stream().filter(X10Device.class::isInstance).map(X10Device.class::cast).toList();
124 public @Nullable Scene getScene(int group) {
125 return scenes.get(group);
128 public boolean hasScene(int group) {
129 return scenes.containsKey(group);
132 public List<Scene> getScenes() {
133 return scenes.values().stream().toList();
136 public @Nullable InsteonScene getInsteonScene(int group) {
137 return (InsteonScene) getScene(group);
140 public List<InsteonScene> getInsteonScenes() {
141 return getScenes().stream().filter(InsteonScene.class::isInstance).map(InsteonScene.class::cast).toList();
144 public @Nullable ProductData getProductData(DeviceAddress address) {
145 Device device = getDevice(address);
146 if (device != null && device.getProductData() != null) {
147 return device.getProductData();
148 } else if (address instanceof InsteonAddress insteonAddress) {
149 return modemDB.getProductData(insteonAddress);
154 public void addDevice(Device device) {
155 devices.put(device.getAddress(), device);
158 public void removeDevice(Device device) {
159 devices.remove(device.getAddress());
162 public void addScene(InsteonScene scene) {
163 scenes.put(scene.getGroup(), scene);
166 public void removeScene(InsteonScene scene) {
167 scenes.remove(scene.getGroup());
170 public void deleteSceneEntries(InsteonDevice device) {
171 getInsteonScenes().stream().filter(scene -> scene.getDevices().contains(device.getAddress()))
172 .forEach(scene -> scene.deleteEntries(device.getAddress()));
175 public void updateSceneEntries(InsteonDevice device) {
176 getInsteonScenes().stream()
177 .filter(scene -> modemDB.getRelatedDevices(scene.getGroup()).contains(device.getAddress()))
178 .forEach(scene -> scene.updateEntries(device));
181 public boolean isInitialized() {
185 public void writeMessage(Msg msg) throws IOException {
186 port.writeMessage(msg);
189 public boolean connect() {
190 logger.debug("connecting to modem");
195 port.registerListener(this);
205 public void disconnect() {
206 logger.debug("disconnecting from modem");
207 if (linker.isRunning()) {
217 public boolean reconnect() {
218 logger.debug("reconnecting to modem");
223 private void discover() {
224 if (isInitialized()) {
225 logger.debug("modem {} already initialized", address);
227 logger.debug("discovering modem");
232 private void getModemInfo() {
234 Msg msg = Msg.makeMessage("GetIMInfo");
236 } catch (IOException e) {
237 logger.warn("error sending modem info query ", e);
238 } catch (InvalidMessageTypeException e) {
239 logger.warn("invalid message ", e);
243 private void handleModemInfo(Msg msg) throws FieldException {
244 InsteonAddress address = msg.getInsteonAddress("IMAddress");
245 int deviceCategory = msg.getInt("DeviceCategory");
246 int subCategory = msg.getInt("DeviceSubCategory");
248 ProductData productData = ProductDataRegistry.getInstance().getProductData(deviceCategory, subCategory);
249 productData.setFirmwareVersion(msg.getInt("FirmwareVersion"));
251 DeviceType deviceType = productData.getDeviceType();
252 if (deviceType == null) {
253 logger.warn("unsupported product data for modem {} devCat:{} subCat:{}", address, deviceCategory,
258 setProductData(productData);
259 instantiateFeatures(deviceType);
260 setFlags(deviceType.getFlags());
264 logger.debug("modem discovered: {}", this);
266 InsteonBridgeHandler handler = getHandler();
267 if (handler != null) {
268 handler.modemDiscovered(this);
272 public void logDeviceStatistics() {
273 logger.debug("devices: {} configured, {} polling, msgs received: {}", getDevices().size(),
274 getPollManager().getSizeOfQueue(), msgsReceived);
278 private void logDevicesAndScenes() {
279 if (!getInsteonDevices().isEmpty()) {
280 logger.debug("configured {} insteon devices", getInsteonDevices().size());
281 if (logger.isTraceEnabled()) {
282 getInsteonDevices().stream().map(String::valueOf).forEach(logger::trace);
285 if (!getX10Devices().isEmpty()) {
286 logger.debug("configured {} x10 devices", getX10Devices().size());
287 if (logger.isTraceEnabled()) {
288 getX10Devices().stream().map(String::valueOf).forEach(logger::trace);
291 if (!getScenes().isEmpty()) {
292 logger.debug("configured {} insteon scenes", getScenes().size());
293 if (logger.isTraceEnabled()) {
294 getScenes().stream().map(String::valueOf).forEach(logger::trace);
300 * Polls related devices to a broadcast group
302 * @param group the broadcast group
303 * @param delay scheduling delay (in milliseconds)
305 public void pollRelatedDevices(int group, long delay) {
306 modemDB.getRelatedDevices(group).stream().map(this::getInsteonDevice).filter(Objects::nonNull)
307 .map(Objects::requireNonNull).forEach(device -> {
308 logger.debug("polling related device {} to broadcast group {}", device.getAddress(), group);
309 device.pollResponders(address, group, delay);
314 * Notifies that the database has been completed
316 public void databaseCompleted() {
317 logger.debug("modem database completed");
319 getDevices().forEach(Device::refresh);
320 getScenes().forEach(Scene::refresh);
322 logDevicesAndScenes();
327 InsteonBridgeHandler handler = getHandler();
328 if (handler != null) {
329 handler.modemDBCompleted();
334 * Notifies that a database link has been updated
336 * @param address the link address
337 * @param group the link group
338 * @param is2Way if two way update
340 public void databaseLinkUpdated(InsteonAddress address, int group, boolean is2Way) {
341 if (!modemDB.isComplete()) {
344 logger.debug("modem database link updated for device {} group {} 2way {}", address, group, is2Way);
346 InsteonDevice device = getInsteonDevice(address);
347 if (device != null) {
349 // set link db to reload on next device poll if still in modem db and is two way update
350 if (device.hasModemDBEntry() && is2Way) {
351 device.getLinkDB().setReload(true);
354 InsteonScene scene = getInsteonScene(group);
358 InsteonBridgeHandler handler = getHandler();
359 if (handler != null) {
360 handler.modemDBLinkUpdated(address, group);
365 * Notifies that a database product data has been updated
367 * @param address the device address
368 * @param productData the updated product data
370 public void databaseProductDataUpdated(InsteonAddress address, ProductData productData) {
371 if (!modemDB.isComplete()) {
374 logger.debug("product data updated for device {} {}", address, productData);
376 InsteonDevice device = getInsteonDevice(address);
377 if (device != null) {
378 device.updateProductData(productData);
380 InsteonBridgeHandler handler = getHandler();
381 if (handler != null) {
382 handler.modemDBProductDataUpdated(address, productData);
387 * Notifies that the modem reset process has been initiated
389 public void resetInitiated() {
390 logger.debug("modem reset initiated");
392 InsteonBridgeHandler handler = getHandler();
393 if (handler != null) {
394 handler.reset(RESET_TIME);
399 * Notifies that the modem port has disconnected
402 public void disconnected() {
403 logger.debug("modem port disconnected");
405 InsteonBridgeHandler handler = getHandler();
406 if (handler != null) {
407 handler.reconnect(this);
412 * Notifies that the modem port has received a message
414 * @param msg the message received
417 public void messageReceived(Msg msg) {
418 if (msg.isPureNack()) {
423 handleX10Message(msg);
424 } else if (msg.isInsteon()) {
425 handleInsteonMessage(msg);
427 handleIMMessage(msg);
429 } catch (FieldException e) {
430 logger.warn("error parsing msg: {}", msg, e);
435 * Notifies that the modem port has sent a message
437 * @param msg the message sent
440 public void messageSent(Msg msg) {
441 if (msg.isAllLinkBroadcast()) {
445 DeviceAddress address = msg.isInsteon() ? msg.getInsteonAddress("toAddress")
446 : msg.isX10Address() ? msg.getX10Address() : msg.isX10Command() ? lastX10Address : getAddress();
447 if (address == null) {
451 lastX10Address = msg.isX10Address() ? (X10Address) address : null;
453 long time = System.currentTimeMillis();
454 Device device = getAddress().equals(address) ? this : getDevice(address);
455 if (device != null) {
456 device.requestSent(msg, time);
458 } catch (FieldException e) {
459 logger.warn("error parsing msg: {}", msg, e);
463 private void handleIMMessage(Msg msg) throws FieldException {
464 if (msg.getCommand() == 0x60) {
465 handleModemInfo(msg);
471 private void handleInsteonMessage(Msg msg) throws FieldException {
472 if (msg.isAllLinkBroadcast() && msg.isReply()) {
475 InsteonAddress toAddr = msg.getInsteonAddress("toAddress");
477 handleMessage(toAddr, msg);
478 } else if (msg.isBroadcast() || msg.isAllLinkBroadcast() || getAddress().equals(toAddr)) {
479 InsteonAddress fromAddr = msg.getInsteonAddress("fromAddress");
480 handleMessage(fromAddr, msg);
484 private void handleX10Message(Msg msg) throws FieldException {
485 X10Address address = lastX10Address;
486 if (msg.isX10Address()) {
487 // store the x10 address to use with the next cmd
488 lastX10Address = msg.getX10Address();
489 } else if (address != null) {
490 handleMessage(address, msg);
491 lastX10Address = null;
495 private void handleMessage(DeviceAddress address, Msg msg) throws FieldException {
496 Device device = getDevice(address);
497 if (device == null) {
498 logger.debug("unknown device with address {}, dropping message", address);
499 } else if (msg.isReply()) {
500 device.requestReplied(msg);
502 device.handleMessage(msg);
508 * Factory method for creating a InsteonModem
510 * @param handler the bridge handler
511 * @param config the bridge config
512 * @param scheduler the scheduler service
513 * @param serialPortManager the serial port manager
514 * @return the newly created InsteonModem
516 public static InsteonModem makeModem(InsteonBridgeHandler handler, InsteonBridgeConfiguration config,
517 ScheduledExecutorService scheduler, SerialPortManager serialPortManager) {
518 InsteonModem modem = new InsteonModem(config, scheduler, serialPortManager);
519 modem.setHandler(handler);