]> git.basschouten.com Git - openhab-addons.git/blob
fa0d041eb915d6ca3e62c8e9a113e0bc949dc816
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 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.nibeheatpump.internal.handler;
14
15 import static org.openhab.binding.nibeheatpump.internal.NibeHeatPumpBindingConstants.*;
16
17 import java.math.BigDecimal;
18 import java.math.RoundingMode;
19 import java.util.ArrayList;
20 import java.util.Collections;
21 import java.util.HashMap;
22 import java.util.List;
23 import java.util.Map;
24 import java.util.concurrent.ScheduledFuture;
25 import java.util.concurrent.TimeUnit;
26 import java.util.concurrent.TimeoutException;
27 import java.util.stream.Collectors;
28
29 import org.openhab.binding.nibeheatpump.internal.NibeHeatPumpCommandResult;
30 import org.openhab.binding.nibeheatpump.internal.NibeHeatPumpException;
31 import org.openhab.binding.nibeheatpump.internal.config.NibeHeatPumpConfiguration;
32 import org.openhab.binding.nibeheatpump.internal.connection.NibeHeatPumpConnector;
33 import org.openhab.binding.nibeheatpump.internal.connection.NibeHeatPumpEventListener;
34 import org.openhab.binding.nibeheatpump.internal.connection.SerialConnector;
35 import org.openhab.binding.nibeheatpump.internal.connection.SimulatorConnector;
36 import org.openhab.binding.nibeheatpump.internal.connection.UDPConnector;
37 import org.openhab.binding.nibeheatpump.internal.message.ModbusDataReadOutMessage;
38 import org.openhab.binding.nibeheatpump.internal.message.ModbusReadRequestMessage;
39 import org.openhab.binding.nibeheatpump.internal.message.ModbusReadResponseMessage;
40 import org.openhab.binding.nibeheatpump.internal.message.ModbusValue;
41 import org.openhab.binding.nibeheatpump.internal.message.ModbusWriteRequestMessage;
42 import org.openhab.binding.nibeheatpump.internal.message.ModbusWriteResponseMessage;
43 import org.openhab.binding.nibeheatpump.internal.message.NibeHeatPumpMessage;
44 import org.openhab.binding.nibeheatpump.internal.models.PumpModel;
45 import org.openhab.binding.nibeheatpump.internal.models.VariableInformation;
46 import org.openhab.binding.nibeheatpump.internal.models.VariableInformation.NibeDataType;
47 import org.openhab.binding.nibeheatpump.internal.models.VariableInformation.Type;
48 import org.openhab.core.io.transport.serial.SerialPortManager;
49 import org.openhab.core.library.types.DecimalType;
50 import org.openhab.core.library.types.OnOffType;
51 import org.openhab.core.library.types.OpenClosedType;
52 import org.openhab.core.library.types.QuantityType;
53 import org.openhab.core.library.types.StringType;
54 import org.openhab.core.library.types.UpDownType;
55 import org.openhab.core.thing.ChannelUID;
56 import org.openhab.core.thing.Thing;
57 import org.openhab.core.thing.ThingStatus;
58 import org.openhab.core.thing.ThingStatusDetail;
59 import org.openhab.core.thing.ThingTypeUID;
60 import org.openhab.core.thing.binding.BaseThingHandler;
61 import org.openhab.core.types.Command;
62 import org.openhab.core.types.RefreshType;
63 import org.openhab.core.types.State;
64 import org.openhab.core.types.UnDefType;
65 import org.slf4j.Logger;
66 import org.slf4j.LoggerFactory;
67
68 /**
69  * The {@link NibeHeatPumpHandler} is responsible for handling commands, which
70  * are sent to one of the channels.
71  *
72  * @author Pauli Anttila - Initial contribution
73  */
74 public class NibeHeatPumpHandler extends BaseThingHandler implements NibeHeatPumpEventListener {
75
76     private static final int TIMEOUT = 4500;
77     private final Logger logger = LoggerFactory.getLogger(NibeHeatPumpHandler.class);
78     private final PumpModel pumpModel;
79     private final SerialPortManager serialPortManager;
80     private final List<Integer> itemsToPoll = Collections.synchronizedList(new ArrayList<>());
81     private final List<Integer> itemsToEnableWrite = new ArrayList<>();
82     private final Map<Integer, CacheObject> stateMap = Collections.synchronizedMap(new HashMap<>());
83     private NibeHeatPumpConfiguration configuration;
84     private NibeHeatPumpConnector connector;
85     private boolean reconnectionRequest;
86     private NibeHeatPumpCommandResult writeResult;
87     private NibeHeatPumpCommandResult readResult;
88     private final Runnable pollingRunnable = new Runnable() {
89         @Override
90         public void run() {
91             if (!configuration.enableReadCommands) {
92                 logger.trace("All read commands denied, skip polling!");
93                 return;
94             }
95
96             List<Integer> items;
97             synchronized (itemsToPoll) {
98                 items = new ArrayList<>(itemsToPoll);
99             }
100
101             for (int item : items) {
102                 if (connector != null && connector.isConnected()
103                         && getThing().getStatusInfo().getStatus() == ThingStatus.ONLINE) {
104                     CacheObject oldValue = stateMap.get(item);
105                     if (oldValue == null
106                             || (oldValue.lastUpdateTime + refreshIntervalMillis()) < System.currentTimeMillis()) {
107                         // it's time to refresh data
108                         logger.debug("Time to refresh variable '{}' data", item);
109
110                         ModbusReadRequestMessage request = new ModbusReadRequestMessage.MessageBuilder()
111                                 .coilAddress(item).build();
112
113                         try {
114                             readResult = sendMessageToNibe(request);
115                             ModbusReadResponseMessage result = (ModbusReadResponseMessage) readResult.get(TIMEOUT,
116                                     TimeUnit.MILLISECONDS);
117                             if (result != null) {
118                                 if (request.getCoilAddress() != result.getCoilAddress()) {
119                                     logger.debug("Data from wrong register '{}' received, expected '{}'",
120                                             result.getCoilAddress(), request.getCoilAddress());
121                                 }
122                                 // update variable anyway
123                                 handleVariableUpdate(pumpModel, result.getValueAsModbusValue());
124                             }
125                         } catch (TimeoutException e) {
126                             logger.debug("Message sending to heat pump failed, no response");
127                             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
128                         } catch (InterruptedException e) {
129                             logger.debug("Message sending to heat pump failed, sending interrupted");
130                         } catch (NibeHeatPumpException e) {
131                             logger.debug("Message sending to heat pump failed, exception {}", e.getMessage());
132                             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
133                         } finally {
134                             readResult = null;
135                         }
136                     }
137                 }
138             }
139         }
140     };
141     private ScheduledFuture<?> connectorTask;
142     private ScheduledFuture<?> pollingJob;
143     private long lastUpdateTime = 0;
144
145     public NibeHeatPumpHandler(Thing thing, PumpModel pumpModel, SerialPortManager serialPortManager) {
146         super(thing);
147         this.pumpModel = pumpModel;
148         this.serialPortManager = serialPortManager;
149     }
150
151     private NibeHeatPumpConnector getConnector() throws NibeHeatPumpException {
152         ThingTypeUID type = thing.getThingTypeUID();
153
154         if (THING_TYPE_F1X45_UDP.equals(type) || THING_TYPE_F1X55_UDP.equals(type) || THING_TYPE_F750_UDP.equals(type)
155                 || THING_TYPE_F470_UDP.equals(type)) {
156             return new UDPConnector();
157         } else if (THING_TYPE_F1X45_SERIAL.equals(type) || THING_TYPE_F1X55_SERIAL.equals(type)
158                 || THING_TYPE_F750_SERIAL.equals(type) || THING_TYPE_F470_SERIAL.equals(type)) {
159             return new SerialConnector(serialPortManager);
160         } else if (THING_TYPE_F1X45_SIMULATOR.equals(type) || THING_TYPE_F1X55_SIMULATOR.equals(type)
161                 || THING_TYPE_F750_SIMULATOR.equals(type) || THING_TYPE_F470_SIMULATOR.equals(type)) {
162             return new SimulatorConnector();
163         }
164
165         String description = String.format("Unknown connector type %s", type);
166         throw new NibeHeatPumpException(description);
167     }
168
169     @Override
170     public void handleCommand(ChannelUID channelUID, Command command) {
171         logger.debug("Received channel: {}, command: {}", channelUID, command);
172
173         int coilAddress = parseCoilAddressFromChannelUID(channelUID);
174
175         if (command.equals(RefreshType.REFRESH)) {
176             logger.debug("Clearing cache value for channel '{}' to refresh channel data", channelUID);
177             clearCache(coilAddress);
178             return;
179         }
180
181         if (!configuration.enableWriteCommands) {
182             logger.info(
183                     "All write commands denied, ignoring command! Change Nibe heat pump binding configuration if you want to enable write commands.");
184             return;
185         }
186
187         if (!itemsToEnableWrite.contains(coilAddress)) {
188             logger.info(
189                     "Write commands to register '{}' not allowed, ignoring command! Add this register to Nibe heat pump binding configuration if you want to enable write commands.",
190                     coilAddress);
191             return;
192         }
193
194         if (connector != null) {
195             VariableInformation variableInfo = VariableInformation.getVariableInfo(pumpModel, coilAddress);
196             logger.debug("Using variable information for register {}: {}", coilAddress, variableInfo);
197
198             if (variableInfo != null && variableInfo.type == VariableInformation.Type.SETTING) {
199                 try {
200                     int value = convertCommandToNibeValue(variableInfo, command);
201
202                     ModbusWriteRequestMessage msg = new ModbusWriteRequestMessage.MessageBuilder()
203                             .coilAddress(coilAddress).value(value).build();
204
205                     writeResult = sendMessageToNibe(msg);
206                     ModbusWriteResponseMessage result = (ModbusWriteResponseMessage) writeResult.get(TIMEOUT,
207                             TimeUnit.MILLISECONDS);
208                     if (result != null) {
209                         if (result.isSuccessfull()) {
210                             logger.debug("Write message sending to heat pump succeeded");
211                         } else {
212                             logger.error("Message sending to heat pump failed, value not accepted by the heat pump");
213                         }
214                     } else {
215                         logger.debug("Something weird happen, result for write command is null");
216                     }
217                 } catch (TimeoutException e) {
218                     logger.warn("Message sending to heat pump failed, no response");
219                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
220                             "No response received from the heat pump");
221                 } catch (InterruptedException e) {
222                     logger.debug("Message sending to heat pump failed, sending interrupted");
223                 } catch (NibeHeatPumpException e) {
224                     logger.debug("Message sending to heat pump failed, exception {}", e.getMessage());
225                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
226                 } catch (CommandTypeNotSupportedException e) {
227                     logger.warn("Unsupported command type {} received for channel {}, coil address {}.",
228                             command.getClass().getName(), channelUID.getId(), coilAddress);
229                 } finally {
230                     writeResult = null;
231                 }
232
233                 // Clear cache value to refresh coil data from the pump.
234                 // We might not know if write message have succeed or not, so let's always refresh it.
235                 logger.debug("Clearing cache value for channel '{}' to refresh channel data", channelUID);
236                 clearCache(coilAddress);
237             } else {
238                 logger.debug("Command to channel '{}' rejected, because item is read only parameter", channelUID);
239             }
240         } else {
241             logger.debug("No connection to heat pump");
242             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_MISSING_ERROR);
243         }
244     }
245
246     @Override
247     public void channelLinked(ChannelUID channelUID) {
248         logger.debug("channelLinked: {}", channelUID);
249
250         // Add channel to polling loop
251         int coilAddress = parseCoilAddressFromChannelUID(channelUID);
252         synchronized (itemsToPoll) {
253             if (!itemsToPoll.contains(coilAddress)) {
254                 logger.debug("New channel '{}' found, register '{}'", channelUID.getAsString(), coilAddress);
255                 itemsToPoll.add(coilAddress);
256             }
257         }
258         clearCache(coilAddress);
259     }
260
261     @Override
262     public void channelUnlinked(ChannelUID channelUID) {
263         logger.debug("channelUnlinked: {}", channelUID);
264
265         // remove channel from polling loop
266         int coilAddress = parseCoilAddressFromChannelUID(channelUID);
267         synchronized (itemsToPoll) {
268             itemsToPoll.removeIf(c -> c.equals(coilAddress));
269         }
270     }
271
272     private int parseCoilAddressFromChannelUID(ChannelUID channelUID) {
273         if (channelUID.getId().contains("#")) {
274             String[] parts = channelUID.getId().split("#");
275             return Integer.parseInt(parts[parts.length - 1]);
276         } else {
277             return Integer.parseInt(channelUID.getId());
278         }
279     }
280
281     @Override
282     public void initialize() {
283         logger.debug("Initialized Nibe Heat Pump device handler for {}", getThing().getUID());
284         configuration = getConfigAs(NibeHeatPumpConfiguration.class);
285         logger.debug("Using configuration: {}", configuration.toString());
286
287         try {
288             parseWriteEnabledItems();
289             connector = getConnector();
290         } catch (IllegalArgumentException | NibeHeatPumpException e) {
291             String description = String.format("Illegal configuration, %s", e.getMessage());
292             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, description);
293             return;
294         }
295
296         itemsToPoll.clear();
297         itemsToPoll.addAll(this.getThing().getChannels().stream().filter(c -> isLinked(c.getUID())).map(c -> {
298             int coilAddress = parseCoilAddressFromChannelUID(c.getUID());
299             logger.debug("Linked channel '{}' found, register '{}'", c.getUID().getAsString(), coilAddress);
300             return coilAddress;
301         }).filter(c -> c != 0).collect(Collectors.toSet()));
302
303         logger.debug("Linked registers {}: {}", itemsToPoll.size(), itemsToPoll);
304
305         clearCache();
306
307         if (connectorTask == null || connectorTask.isCancelled()) {
308             connectorTask = scheduler.scheduleWithFixedDelay(() -> {
309                 if (reconnectionRequest) {
310                     logger.debug("Restarting requested, restarting...");
311                     reconnectionRequest = false;
312                     closeConnection();
313                 }
314
315                 logger.debug("Checking Nibe Heat pump connection, thing status = {}", thing.getStatus());
316                 connect();
317             }, 0, 10, TimeUnit.SECONDS);
318         }
319     }
320
321     private void connect() {
322         if (!connector.isConnected()) {
323             logger.debug("Connecting to heat pump");
324             try {
325                 connector.addEventListener(this);
326                 connector.connect(configuration);
327                 updateStatus(ThingStatus.ONLINE);
328
329                 if (pollingJob == null || pollingJob.isCancelled()) {
330                     logger.debug("Start refresh task, interval={}sec", 1);
331                     pollingJob = scheduler.scheduleWithFixedDelay(pollingRunnable, 0, 1, TimeUnit.SECONDS);
332                 }
333             } catch (NibeHeatPumpException e) {
334                 logger.debug("Error occurred when connecting to heat pump, exception {}", e.getMessage());
335                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
336             }
337         } else {
338             logger.debug("Connection to heat pump already open");
339         }
340     }
341
342     @Override
343     public void dispose() {
344         logger.debug("Thing {} disposed.", getThing().getUID());
345
346         if (connectorTask != null && !connectorTask.isCancelled()) {
347             connectorTask.cancel(true);
348             connectorTask = null;
349         }
350
351         closeConnection();
352     }
353
354     private void closeConnection() {
355         logger.debug("Closing connection to the heat pump");
356
357         if (pollingJob != null && !pollingJob.isCancelled()) {
358             pollingJob.cancel(true);
359             pollingJob = null;
360         }
361
362         if (connector != null) {
363             connector.removeEventListener(this);
364             connector.disconnect();
365         }
366     }
367
368     private long refreshIntervalMillis() {
369         return configuration.refreshInterval * 1000;
370     }
371
372     private int convertCommandToNibeValue(VariableInformation variableInfo, Command command)
373             throws CommandTypeNotSupportedException {
374         int value;
375
376         if (command instanceof DecimalType || command instanceof QuantityType || command instanceof StringType) {
377             BigDecimal v;
378             if (command instanceof DecimalType) {
379                 v = ((DecimalType) command).toBigDecimal();
380             } else if (command instanceof QuantityType) {
381                 v = ((QuantityType) command).toBigDecimal();
382             } else {
383                 v = new BigDecimal(command.toString());
384             }
385             int decimals = (int) Math.log10(variableInfo.factor);
386             value = v.movePointRight(decimals).intValue();
387         } else if ((command instanceof OnOffType || command instanceof OpenClosedType || command instanceof UpDownType)
388                 && variableInfo.factor == 1) {
389             value = (command.equals(OnOffType.ON) || command.equals(UpDownType.UP)
390                     || command.equals(OpenClosedType.OPEN)) ? 1 : 0;
391         } else {
392             throw new CommandTypeNotSupportedException();
393         }
394
395         return value;
396     }
397
398     private void parseWriteEnabledItems() throws IllegalArgumentException {
399         itemsToEnableWrite.clear();
400         if (configuration.enableWriteCommands && configuration.enableWriteCommandsToRegisters != null
401                 && configuration.enableWriteCommandsToRegisters.length() > 0) {
402             String[] items = configuration.enableWriteCommandsToRegisters.replace(" ", "").split(",");
403             for (String item : items) {
404                 try {
405                     int coilAddress = Integer.parseInt(item);
406                     VariableInformation variableInformation = VariableInformation.getVariableInfo(pumpModel,
407                             coilAddress);
408                     if (variableInformation == null) {
409                         String description = String.format("Unknown register %s", coilAddress);
410                         throw new IllegalArgumentException(description);
411                     }
412                     itemsToEnableWrite.add(coilAddress);
413                 } catch (NumberFormatException e) {
414                     String description = String.format("Illegal register %s", item);
415                     throw new IllegalArgumentException(description);
416                 }
417             }
418         }
419         logger.debug("Enabled registers for write commands: {}", itemsToEnableWrite);
420     }
421
422     private State convertNibeValueToState(VariableInformation variableInfo, int value, String acceptedItemType) {
423         State state = UnDefType.UNDEF;
424         long x;
425
426         NibeDataType dataType = variableInfo.dataType;
427         int decimals = (int) Math.log10(variableInfo.factor);
428         switch (dataType) {
429             case U8:
430                 x = Byte.toUnsignedLong((byte) (value & 0xFF));
431                 break;
432             case U16:
433                 x = Short.toUnsignedLong((short) (value & 0xFFFF));
434                 break;
435             case U32:
436                 x = Integer.toUnsignedLong(value);
437                 break;
438             case S8:
439                 x = (byte) (value & 0xFF);
440                 break;
441             case S16:
442                 x = (short) (value & 0xFFFF);
443                 break;
444             case S32:
445                 x = value;
446                 break;
447             default:
448                 return state;
449         }
450         BigDecimal converted = new BigDecimal(x).movePointLeft(decimals).setScale(decimals, RoundingMode.HALF_EVEN);
451
452         if ("String".equalsIgnoreCase(acceptedItemType)) {
453             state = new StringType(converted.toString());
454
455         } else if ("Switch".equalsIgnoreCase(acceptedItemType)) {
456             state = converted.intValue() == 0 ? OnOffType.OFF : OnOffType.ON;
457
458         } else if ("Number".equalsIgnoreCase(acceptedItemType)) {
459             state = new DecimalType(converted);
460         }
461
462         return state;
463     }
464
465     private void clearCache() {
466         stateMap.clear();
467         lastUpdateTime = 0;
468     }
469
470     private void clearCache(int coilAddress) {
471         stateMap.put(coilAddress, null);
472     }
473
474     private synchronized NibeHeatPumpCommandResult sendMessageToNibe(NibeHeatPumpMessage msg)
475             throws NibeHeatPumpException {
476         logger.debug("Sending message: {}", msg);
477         connector.sendDatagram(msg);
478         return new NibeHeatPumpCommandResult();
479     }
480
481     @Override
482     public void msgReceived(NibeHeatPumpMessage msg) {
483         try {
484             if (logger.isTraceEnabled()) {
485                 logger.trace("Received raw data: {}", msg.toHexString());
486             }
487
488             logger.debug("Received message: {}", msg);
489
490             updateStatus(ThingStatus.ONLINE);
491
492             if (msg instanceof ModbusReadResponseMessage) {
493                 handleReadResponseMessage((ModbusReadResponseMessage) msg);
494             } else if (msg instanceof ModbusWriteResponseMessage) {
495                 handleWriteResponseMessage((ModbusWriteResponseMessage) msg);
496             } else if (msg instanceof ModbusDataReadOutMessage) {
497                 handleDataReadOutMessage((ModbusDataReadOutMessage) msg);
498             } else {
499                 logger.debug("Received unknown message: {}", msg.toString());
500             }
501         } catch (Exception e) {
502             logger.debug("Error occurred when parsing received message, reason: {}", e.getMessage());
503         }
504     }
505
506     @Override
507     public void errorOccurred(String error) {
508         logger.debug("Error '{}' occurred, re-establish the connection", error);
509         reconnectionRequest = true;
510         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, error);
511     }
512
513     private void handleReadResponseMessage(ModbusReadResponseMessage msg) {
514         if (readResult != null) {
515             readResult.set(msg);
516         }
517     }
518
519     private void handleWriteResponseMessage(ModbusWriteResponseMessage msg) {
520         if (writeResult != null) {
521             writeResult.set(msg);
522         }
523     }
524
525     private void handleDataReadOutMessage(ModbusDataReadOutMessage msg) {
526         boolean parse = true;
527
528         logger.debug("Received data read out message");
529         if (configuration.throttleTime > 0) {
530             if ((lastUpdateTime + configuration.throttleTime) > System.currentTimeMillis()) {
531                 logger.debug("Skipping data read out message parsing");
532                 parse = false;
533             }
534         }
535
536         if (parse) {
537             logger.debug("Parsing data read out message");
538             lastUpdateTime = System.currentTimeMillis();
539             List<ModbusValue> regValues = msg.getValues();
540
541             if (regValues != null) {
542                 for (ModbusValue val : regValues) {
543                     handleVariableUpdate(pumpModel, val);
544                 }
545             }
546         }
547     }
548
549     private void handleVariableUpdate(PumpModel pumpModel, ModbusValue value) {
550         logger.debug("Received variable update: {}", value);
551         int coilAddress = value.getCoilAddress();
552
553         VariableInformation variableInfo = VariableInformation.getVariableInfo(pumpModel, coilAddress);
554
555         if (variableInfo != null) {
556             logger.trace("Using variable information to register {}: {}", coilAddress, variableInfo);
557
558             int val = value.getValue();
559             logger.debug("{} = {}", coilAddress + ":" + variableInfo.variable + "/" + variableInfo.factor, val);
560
561             CacheObject oldValue = stateMap.get(coilAddress);
562
563             if (oldValue != null && val == oldValue.value
564                     && (oldValue.lastUpdateTime + refreshIntervalMillis() / 2) >= System.currentTimeMillis()) {
565                 logger.trace("Value did not change, ignoring update");
566             } else {
567                 final String channelPrefix = (variableInfo.type == Type.SETTING ? "setting#" : "sensor#");
568                 final String channelId = channelPrefix + String.valueOf(coilAddress);
569                 final String acceptedItemType = thing.getChannel(channelId).getAcceptedItemType();
570
571                 logger.trace("AcceptedItemType for channel {} = {}", channelId, acceptedItemType);
572                 State state = convertNibeValueToState(variableInfo, val, acceptedItemType);
573                 logger.debug("Setting state {} = {}", coilAddress + ":" + variableInfo.variable, state);
574                 stateMap.put(coilAddress, new CacheObject(System.currentTimeMillis(), val));
575                 updateState(new ChannelUID(getThing().getUID(), channelId), state);
576             }
577         } else {
578             logger.debug("Unknown register {}", coilAddress);
579         }
580     }
581
582     protected class CacheObject {
583
584         /** Time when cache object updated in milliseconds */
585         final long lastUpdateTime;
586
587         /** Cache value */
588         final int value;
589
590         /**
591          * Initialize cache object.
592          *
593          * @param lastUpdateTime Time in milliseconds.
594          * @param value Cache value.
595          */
596         CacheObject(long lastUpdateTime, int value) {
597             this.lastUpdateTime = lastUpdateTime;
598             this.value = value;
599         }
600     }
601 }