]> git.basschouten.com Git - openhab-addons.git/blob
b41212d553a216adc5863ea93a0a0ee2d486a975
[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;
14
15 import java.io.IOException;
16 import java.util.List;
17 import java.util.Map;
18 import java.util.Objects;
19 import java.util.concurrent.ConcurrentHashMap;
20 import java.util.concurrent.ScheduledExecutorService;
21
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;
34
35 /**
36  * The {@link InsteonModem} represents an Insteom modem
37  *
38  * @author Jeremy Setton - Initial contribution
39  */
40 @NonNullByDefault
41 public class InsteonModem extends BaseDevice<InsteonAddress, InsteonBridgeHandler> implements PortListener {
42     private static final int RESET_TIME = 20; // in seconds
43
44     private Port port;
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;
55
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);
65     }
66
67     @Override
68     public @Nullable InsteonModem getModem() {
69         return this;
70     }
71
72     public Port getPort() {
73         return port;
74     }
75
76     public ModemDB getDB() {
77         return modemDB;
78     }
79
80     public DatabaseManager getDBM() {
81         return dbm;
82     }
83
84     public LinkManager getLinkManager() {
85         return linker;
86     }
87
88     public PollManager getPollManager() {
89         return poller;
90     }
91
92     public RequestManager getRequestManager() {
93         return requester;
94     }
95
96     public @Nullable Device getDevice(DeviceAddress address) {
97         return devices.get(address);
98     }
99
100     public boolean hasDevice(DeviceAddress address) {
101         return devices.containsKey(address);
102     }
103
104     public List<Device> getDevices() {
105         return devices.values().stream().toList();
106     }
107
108     public @Nullable InsteonDevice getInsteonDevice(InsteonAddress address) {
109         return (InsteonDevice) getDevice(address);
110     }
111
112     public List<InsteonDevice> getInsteonDevices() {
113         return getDevices().stream().filter(InsteonDevice.class::isInstance).map(InsteonDevice.class::cast).toList();
114     }
115
116     public @Nullable X10Device getX10Device(X10Address address) {
117         return (X10Device) getDevice(address);
118     }
119
120     public List<X10Device> getX10Devices() {
121         return getDevices().stream().filter(X10Device.class::isInstance).map(X10Device.class::cast).toList();
122     }
123
124     public @Nullable Scene getScene(int group) {
125         return scenes.get(group);
126     }
127
128     public boolean hasScene(int group) {
129         return scenes.containsKey(group);
130     }
131
132     public List<Scene> getScenes() {
133         return scenes.values().stream().toList();
134     }
135
136     public @Nullable InsteonScene getInsteonScene(int group) {
137         return (InsteonScene) getScene(group);
138     }
139
140     public List<InsteonScene> getInsteonScenes() {
141         return getScenes().stream().filter(InsteonScene.class::isInstance).map(InsteonScene.class::cast).toList();
142     }
143
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);
150         }
151         return null;
152     }
153
154     public void addDevice(Device device) {
155         devices.put(device.getAddress(), device);
156     }
157
158     public void removeDevice(Device device) {
159         devices.remove(device.getAddress());
160     }
161
162     public void addScene(InsteonScene scene) {
163         scenes.put(scene.getGroup(), scene);
164     }
165
166     public void removeScene(InsteonScene scene) {
167         scenes.remove(scene.getGroup());
168     }
169
170     public void deleteSceneEntries(InsteonDevice device) {
171         getInsteonScenes().stream().filter(scene -> scene.getDevices().contains(device.getAddress()))
172                 .forEach(scene -> scene.deleteEntries(device.getAddress()));
173     }
174
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));
179     }
180
181     public boolean isInitialized() {
182         return initialized;
183     }
184
185     public void writeMessage(Msg msg) throws IOException {
186         port.writeMessage(msg);
187     }
188
189     public boolean connect() {
190         logger.debug("connecting to modem");
191         if (!port.start()) {
192             return false;
193         }
194
195         port.registerListener(this);
196
197         poller.start();
198         requester.start();
199
200         discover();
201
202         return true;
203     }
204
205     public void disconnect() {
206         logger.debug("disconnecting from modem");
207         if (linker.isRunning()) {
208             linker.stop();
209         }
210
211         dbm.stop();
212         port.stop();
213         requester.stop();
214         poller.stop();
215     }
216
217     public boolean reconnect() {
218         logger.debug("reconnecting to modem");
219         port.stop();
220         return port.start();
221     }
222
223     private void discover() {
224         if (isInitialized()) {
225             logger.debug("modem {} already initialized", address);
226         } else {
227             logger.debug("discovering modem");
228             getModemInfo();
229         }
230     }
231
232     private void getModemInfo() {
233         try {
234             Msg msg = Msg.makeMessage("GetIMInfo");
235             writeMessage(msg);
236         } catch (IOException e) {
237             logger.warn("error sending modem info query ", e);
238         } catch (InvalidMessageTypeException e) {
239             logger.warn("invalid message ", e);
240         }
241     }
242
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");
247
248         ProductData productData = ProductDataRegistry.getInstance().getProductData(deviceCategory, subCategory);
249         productData.setFirmwareVersion(msg.getInt("FirmwareVersion"));
250
251         DeviceType deviceType = productData.getDeviceType();
252         if (deviceType == null) {
253             logger.warn("unsupported product data for modem {} devCat:{} subCat:{}", address, deviceCategory,
254                     subCategory);
255             return;
256         }
257         setAddress(address);
258         setProductData(productData);
259         instantiateFeatures(deviceType);
260         setFlags(deviceType.getFlags());
261
262         initialized = true;
263
264         logger.debug("modem discovered: {}", this);
265
266         InsteonBridgeHandler handler = getHandler();
267         if (handler != null) {
268             handler.modemDiscovered(this);
269         }
270     }
271
272     public void logDeviceStatistics() {
273         logger.debug("devices: {} configured, {} polling, msgs received: {}", getDevices().size(),
274                 getPollManager().getSizeOfQueue(), msgsReceived);
275         msgsReceived = 0;
276     }
277
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);
283             }
284         }
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);
289             }
290         }
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);
295             }
296         }
297     }
298
299     /**
300      * Polls related devices to a broadcast group
301      *
302      * @param group the broadcast group
303      * @param delay scheduling delay (in milliseconds)
304      */
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);
310                 });
311     }
312
313     /**
314      * Notifies that the database has been completed
315      */
316     public void databaseCompleted() {
317         logger.debug("modem database completed");
318
319         getDevices().forEach(Device::refresh);
320         getScenes().forEach(Scene::refresh);
321
322         logDevicesAndScenes();
323
324         startPolling();
325         refresh();
326
327         InsteonBridgeHandler handler = getHandler();
328         if (handler != null) {
329             handler.modemDBCompleted();
330         }
331     }
332
333     /**
334      * Notifies that a database link has been updated
335      *
336      * @param address the link address
337      * @param group the link group
338      * @param is2Way if two way update
339      */
340     public void databaseLinkUpdated(InsteonAddress address, int group, boolean is2Way) {
341         if (!modemDB.isComplete()) {
342             return;
343         }
344         logger.debug("modem database link updated for device {} group {} 2way {}", address, group, is2Way);
345
346         InsteonDevice device = getInsteonDevice(address);
347         if (device != null) {
348             device.refresh();
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);
352             }
353         }
354         InsteonScene scene = getInsteonScene(group);
355         if (scene != null) {
356             scene.refresh();
357         }
358         InsteonBridgeHandler handler = getHandler();
359         if (handler != null) {
360             handler.modemDBLinkUpdated(address, group);
361         }
362     }
363
364     /**
365      * Notifies that a database product data has been updated
366      *
367      * @param address the device address
368      * @param productData the updated product data
369      */
370     public void databaseProductDataUpdated(InsteonAddress address, ProductData productData) {
371         if (!modemDB.isComplete()) {
372             return;
373         }
374         logger.debug("product data updated for device {} {}", address, productData);
375
376         InsteonDevice device = getInsteonDevice(address);
377         if (device != null) {
378             device.updateProductData(productData);
379         }
380         InsteonBridgeHandler handler = getHandler();
381         if (handler != null) {
382             handler.modemDBProductDataUpdated(address, productData);
383         }
384     }
385
386     /**
387      * Notifies that the modem reset process has been initiated
388      */
389     public void resetInitiated() {
390         logger.debug("modem reset initiated");
391
392         InsteonBridgeHandler handler = getHandler();
393         if (handler != null) {
394             handler.reset(RESET_TIME);
395         }
396     }
397
398     /**
399      * Notifies that the modem port has disconnected
400      */
401     @Override
402     public void disconnected() {
403         logger.debug("modem port disconnected");
404
405         InsteonBridgeHandler handler = getHandler();
406         if (handler != null) {
407             handler.reconnect(this);
408         }
409     }
410
411     /**
412      * Notifies that the modem port has received a message
413      *
414      * @param msg the message received
415      */
416     @Override
417     public void messageReceived(Msg msg) {
418         if (msg.isPureNack()) {
419             return;
420         }
421         try {
422             if (msg.isX10()) {
423                 handleX10Message(msg);
424             } else if (msg.isInsteon()) {
425                 handleInsteonMessage(msg);
426             } else {
427                 handleIMMessage(msg);
428             }
429         } catch (FieldException e) {
430             logger.warn("error parsing msg: {}", msg, e);
431         }
432     }
433
434     /**
435      * Notifies that the modem port has sent a message
436      *
437      * @param msg the message sent
438      */
439     @Override
440     public void messageSent(Msg msg) {
441         if (msg.isAllLinkBroadcast()) {
442             return;
443         }
444         try {
445             DeviceAddress address = msg.isInsteon() ? msg.getInsteonAddress("toAddress")
446                     : msg.isX10Address() ? msg.getX10Address() : msg.isX10Command() ? lastX10Address : getAddress();
447             if (address == null) {
448                 return;
449             }
450             if (msg.isX10()) {
451                 lastX10Address = msg.isX10Address() ? (X10Address) address : null;
452             }
453             long time = System.currentTimeMillis();
454             Device device = getAddress().equals(address) ? this : getDevice(address);
455             if (device != null) {
456                 device.requestSent(msg, time);
457             }
458         } catch (FieldException e) {
459             logger.warn("error parsing msg: {}", msg, e);
460         }
461     }
462
463     private void handleIMMessage(Msg msg) throws FieldException {
464         if (msg.getCommand() == 0x60) {
465             handleModemInfo(msg);
466         } else {
467             handleMessage(msg);
468         }
469     }
470
471     private void handleInsteonMessage(Msg msg) throws FieldException {
472         if (msg.isAllLinkBroadcast() && msg.isReply()) {
473             return;
474         }
475         InsteonAddress toAddr = msg.getInsteonAddress("toAddress");
476         if (msg.isReply()) {
477             handleMessage(toAddr, msg);
478         } else if (msg.isBroadcast() || msg.isAllLinkBroadcast() || getAddress().equals(toAddr)) {
479             InsteonAddress fromAddr = msg.getInsteonAddress("fromAddress");
480             handleMessage(fromAddr, msg);
481         }
482     }
483
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;
492         }
493     }
494
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);
501         } else {
502             device.handleMessage(msg);
503             msgsReceived++;
504         }
505     }
506
507     /**
508      * Factory method for creating a InsteonModem
509      *
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
515      */
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);
520         return modem;
521     }
522 }