]> git.basschouten.com Git - openhab-addons.git/blob
1b9b964a29a74e12d266ebe82b23d5351f2e7f13
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 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_SMO40_UDP.equals(type)
155                 || THING_TYPE_F750_UDP.equals(type) || 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_SMO40_SERIAL.equals(type) || THING_TYPE_F750_SERIAL.equals(type)
159                 || THING_TYPE_F470_SERIAL.equals(type)) {
160             return new SerialConnector(serialPortManager);
161         } else if (THING_TYPE_F1X45_SIMULATOR.equals(type) || THING_TYPE_F1X55_SIMULATOR.equals(type)
162                 || THING_TYPE_SMO40_SIMULATOR.equals(type) || THING_TYPE_F750_SIMULATOR.equals(type)
163                 || THING_TYPE_F470_SIMULATOR.equals(type)) {
164             return new SimulatorConnector();
165         }
166
167         String description = String.format("Unknown connector type %s", type);
168         throw new NibeHeatPumpException(description);
169     }
170
171     @Override
172     public void handleCommand(ChannelUID channelUID, Command command) {
173         logger.debug("Received channel: {}, command: {}", channelUID, command);
174
175         int coilAddress = parseCoilAddressFromChannelUID(channelUID);
176
177         if (command.equals(RefreshType.REFRESH)) {
178             logger.debug("Clearing cache value for channel '{}' to refresh channel data", channelUID);
179             clearCache(coilAddress);
180             return;
181         }
182
183         if (!configuration.enableWriteCommands) {
184             logger.info(
185                     "All write commands denied, ignoring command! Change Nibe heat pump binding configuration if you want to enable write commands.");
186             return;
187         }
188
189         if (!itemsToEnableWrite.contains(coilAddress)) {
190             logger.info(
191                     "Write commands to register '{}' not allowed, ignoring command! Add this register to Nibe heat pump binding configuration if you want to enable write commands.",
192                     coilAddress);
193             return;
194         }
195
196         if (connector != null) {
197             VariableInformation variableInfo = VariableInformation.getVariableInfo(pumpModel, coilAddress);
198             logger.debug("Using variable information for register {}: {}", coilAddress, variableInfo);
199
200             if (variableInfo != null && variableInfo.type == VariableInformation.Type.SETTING) {
201                 try {
202                     int value = convertCommandToNibeValue(variableInfo, command);
203
204                     ModbusWriteRequestMessage msg = new ModbusWriteRequestMessage.MessageBuilder()
205                             .coilAddress(coilAddress).value(value).build();
206
207                     writeResult = sendMessageToNibe(msg);
208                     ModbusWriteResponseMessage result = (ModbusWriteResponseMessage) writeResult.get(TIMEOUT,
209                             TimeUnit.MILLISECONDS);
210                     if (result != null) {
211                         if (result.isSuccessfull()) {
212                             logger.debug("Write message sending to heat pump succeeded");
213                         } else {
214                             logger.error("Message sending to heat pump failed, value not accepted by the heat pump");
215                         }
216                     } else {
217                         logger.debug("Something weird happen, result for write command is null");
218                     }
219                 } catch (TimeoutException e) {
220                     logger.warn("Message sending to heat pump failed, no response");
221                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
222                             "No response received from the heat pump");
223                 } catch (InterruptedException e) {
224                     logger.debug("Message sending to heat pump failed, sending interrupted");
225                 } catch (NibeHeatPumpException e) {
226                     logger.debug("Message sending to heat pump failed, exception {}", e.getMessage());
227                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
228                 } catch (CommandTypeNotSupportedException e) {
229                     logger.warn("Unsupported command type {} received for channel {}, coil address {}.",
230                             command.getClass().getName(), channelUID.getId(), coilAddress);
231                 } finally {
232                     writeResult = null;
233                 }
234
235                 // Clear cache value to refresh coil data from the pump.
236                 // We might not know if write message have succeed or not, so let's always refresh it.
237                 logger.debug("Clearing cache value for channel '{}' to refresh channel data", channelUID);
238                 clearCache(coilAddress);
239             } else {
240                 logger.debug("Command to channel '{}' rejected, because item is read only parameter", channelUID);
241             }
242         } else {
243             logger.debug("No connection to heat pump");
244             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_MISSING_ERROR);
245         }
246     }
247
248     @Override
249     public void channelLinked(ChannelUID channelUID) {
250         logger.debug("channelLinked: {}", channelUID);
251
252         // Add channel to polling loop
253         int coilAddress = parseCoilAddressFromChannelUID(channelUID);
254         synchronized (itemsToPoll) {
255             if (!itemsToPoll.contains(coilAddress)) {
256                 logger.debug("New channel '{}' found, register '{}'", channelUID.getAsString(), coilAddress);
257                 itemsToPoll.add(coilAddress);
258             }
259         }
260         clearCache(coilAddress);
261     }
262
263     @Override
264     public void channelUnlinked(ChannelUID channelUID) {
265         logger.debug("channelUnlinked: {}", channelUID);
266
267         // remove channel from polling loop
268         int coilAddress = parseCoilAddressFromChannelUID(channelUID);
269         synchronized (itemsToPoll) {
270             itemsToPoll.removeIf(c -> c.equals(coilAddress));
271         }
272     }
273
274     private int parseCoilAddressFromChannelUID(ChannelUID channelUID) {
275         if (channelUID.getId().contains("#")) {
276             String[] parts = channelUID.getId().split("#");
277             return Integer.parseInt(parts[parts.length - 1]);
278         } else {
279             return Integer.parseInt(channelUID.getId());
280         }
281     }
282
283     @Override
284     public void initialize() {
285         logger.debug("Initialized Nibe Heat Pump device handler for {}", getThing().getUID());
286         configuration = getConfigAs(NibeHeatPumpConfiguration.class);
287         logger.debug("Using configuration: {}", configuration.toString());
288
289         try {
290             parseWriteEnabledItems();
291             connector = getConnector();
292         } catch (IllegalArgumentException | NibeHeatPumpException e) {
293             String description = String.format("Illegal configuration, %s", e.getMessage());
294             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, description);
295             return;
296         }
297
298         itemsToPoll.clear();
299         itemsToPoll.addAll(this.getThing().getChannels().stream().filter(c -> isLinked(c.getUID())).map(c -> {
300             int coilAddress = parseCoilAddressFromChannelUID(c.getUID());
301             logger.debug("Linked channel '{}' found, register '{}'", c.getUID().getAsString(), coilAddress);
302             return coilAddress;
303         }).filter(c -> c != 0).collect(Collectors.toSet()));
304
305         logger.debug("Linked registers {}: {}", itemsToPoll.size(), itemsToPoll);
306
307         clearCache();
308
309         if (connectorTask == null || connectorTask.isCancelled()) {
310             connectorTask = scheduler.scheduleWithFixedDelay(() -> {
311                 if (reconnectionRequest) {
312                     logger.debug("Restarting requested, restarting...");
313                     reconnectionRequest = false;
314                     closeConnection();
315                 }
316
317                 logger.debug("Checking Nibe Heat pump connection, thing status = {}", thing.getStatus());
318                 connect();
319             }, 0, 10, TimeUnit.SECONDS);
320         }
321     }
322
323     private void connect() {
324         if (!connector.isConnected()) {
325             logger.debug("Connecting to heat pump");
326             try {
327                 connector.addEventListener(this);
328                 connector.connect(configuration);
329                 updateStatus(ThingStatus.ONLINE);
330
331                 if (pollingJob == null || pollingJob.isCancelled()) {
332                     logger.debug("Start refresh task, interval={}sec", 1);
333                     pollingJob = scheduler.scheduleWithFixedDelay(pollingRunnable, 0, 1, TimeUnit.SECONDS);
334                 }
335             } catch (NibeHeatPumpException e) {
336                 logger.debug("Error occurred when connecting to heat pump, exception {}", e.getMessage());
337                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
338             }
339         } else {
340             logger.debug("Connection to heat pump already open");
341         }
342     }
343
344     @Override
345     public void dispose() {
346         logger.debug("Thing {} disposed.", getThing().getUID());
347
348         if (connectorTask != null && !connectorTask.isCancelled()) {
349             connectorTask.cancel(true);
350             connectorTask = null;
351         }
352
353         closeConnection();
354     }
355
356     private void closeConnection() {
357         logger.debug("Closing connection to the heat pump");
358
359         if (pollingJob != null && !pollingJob.isCancelled()) {
360             pollingJob.cancel(true);
361             pollingJob = null;
362         }
363
364         if (connector != null) {
365             connector.removeEventListener(this);
366             connector.disconnect();
367         }
368     }
369
370     private long refreshIntervalMillis() {
371         return configuration.refreshInterval * 1000;
372     }
373
374     private int convertCommandToNibeValue(VariableInformation variableInfo, Command command)
375             throws CommandTypeNotSupportedException {
376         int value;
377
378         if (command instanceof DecimalType || command instanceof QuantityType || command instanceof StringType) {
379             BigDecimal v;
380             if (command instanceof DecimalType) {
381                 v = ((DecimalType) command).toBigDecimal();
382             } else if (command instanceof QuantityType) {
383                 v = ((QuantityType) command).toBigDecimal();
384             } else {
385                 v = new BigDecimal(command.toString());
386             }
387             int decimals = (int) Math.log10(variableInfo.factor);
388             value = v.movePointRight(decimals).intValue();
389         } else if ((command instanceof OnOffType || command instanceof OpenClosedType || command instanceof UpDownType)
390                 && variableInfo.factor == 1) {
391             value = (command.equals(OnOffType.ON) || command.equals(UpDownType.UP)
392                     || command.equals(OpenClosedType.OPEN)) ? 1 : 0;
393         } else {
394             throw new CommandTypeNotSupportedException();
395         }
396
397         return value;
398     }
399
400     private void parseWriteEnabledItems() throws IllegalArgumentException {
401         itemsToEnableWrite.clear();
402         if (configuration.enableWriteCommands && configuration.enableWriteCommandsToRegisters != null
403                 && configuration.enableWriteCommandsToRegisters.length() > 0) {
404             String[] items = configuration.enableWriteCommandsToRegisters.replace(" ", "").split(",");
405             for (String item : items) {
406                 try {
407                     int coilAddress = Integer.parseInt(item);
408                     VariableInformation variableInformation = VariableInformation.getVariableInfo(pumpModel,
409                             coilAddress);
410                     if (variableInformation == null) {
411                         String description = String.format("Unknown register %s", coilAddress);
412                         throw new IllegalArgumentException(description);
413                     }
414                     itemsToEnableWrite.add(coilAddress);
415                 } catch (NumberFormatException e) {
416                     String description = String.format("Illegal register %s", item);
417                     throw new IllegalArgumentException(description);
418                 }
419             }
420         }
421         logger.debug("Enabled registers for write commands: {}", itemsToEnableWrite);
422     }
423
424     private State convertNibeValueToState(VariableInformation variableInfo, int value, String acceptedItemType) {
425         State state = UnDefType.UNDEF;
426         long x;
427
428         NibeDataType dataType = variableInfo.dataType;
429         int decimals = (int) Math.log10(variableInfo.factor);
430         switch (dataType) {
431             case U8:
432                 x = Byte.toUnsignedLong((byte) (value & 0xFF));
433                 break;
434             case U16:
435                 x = Short.toUnsignedLong((short) (value & 0xFFFF));
436                 break;
437             case U32:
438                 x = Integer.toUnsignedLong(value);
439                 break;
440             case S8:
441                 x = (byte) (value & 0xFF);
442                 break;
443             case S16:
444                 x = (short) (value & 0xFFFF);
445                 break;
446             case S32:
447                 x = value;
448                 break;
449             default:
450                 return state;
451         }
452         BigDecimal converted = new BigDecimal(x).movePointLeft(decimals).setScale(decimals, RoundingMode.HALF_EVEN);
453
454         if ("String".equalsIgnoreCase(acceptedItemType)) {
455             state = new StringType(converted.toString());
456
457         } else if ("Switch".equalsIgnoreCase(acceptedItemType)) {
458             state = converted.intValue() == 0 ? OnOffType.OFF : OnOffType.ON;
459
460         } else if ("Number".equalsIgnoreCase(acceptedItemType)) {
461             state = new DecimalType(converted);
462         }
463
464         return state;
465     }
466
467     private void clearCache() {
468         stateMap.clear();
469         lastUpdateTime = 0;
470     }
471
472     private void clearCache(int coilAddress) {
473         stateMap.put(coilAddress, null);
474     }
475
476     private synchronized NibeHeatPumpCommandResult sendMessageToNibe(NibeHeatPumpMessage msg)
477             throws NibeHeatPumpException {
478         logger.debug("Sending message: {}", msg);
479         connector.sendDatagram(msg);
480         return new NibeHeatPumpCommandResult();
481     }
482
483     @Override
484     public void msgReceived(NibeHeatPumpMessage msg) {
485         try {
486             if (logger.isTraceEnabled()) {
487                 logger.trace("Received raw data: {}", msg.toHexString());
488             }
489
490             logger.debug("Received message: {}", msg);
491
492             updateStatus(ThingStatus.ONLINE);
493
494             if (msg instanceof ModbusReadResponseMessage) {
495                 handleReadResponseMessage((ModbusReadResponseMessage) msg);
496             } else if (msg instanceof ModbusWriteResponseMessage) {
497                 handleWriteResponseMessage((ModbusWriteResponseMessage) msg);
498             } else if (msg instanceof ModbusDataReadOutMessage) {
499                 handleDataReadOutMessage((ModbusDataReadOutMessage) msg);
500             } else {
501                 logger.debug("Received unknown message: {}", msg.toString());
502             }
503         } catch (Exception e) {
504             logger.debug("Error occurred when parsing received message, reason: {}", e.getMessage());
505         }
506     }
507
508     @Override
509     public void errorOccurred(String error) {
510         logger.debug("Error '{}' occurred, re-establish the connection", error);
511         reconnectionRequest = true;
512         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, error);
513     }
514
515     private void handleReadResponseMessage(ModbusReadResponseMessage msg) {
516         if (readResult != null) {
517             readResult.set(msg);
518         }
519     }
520
521     private void handleWriteResponseMessage(ModbusWriteResponseMessage msg) {
522         if (writeResult != null) {
523             writeResult.set(msg);
524         }
525     }
526
527     private void handleDataReadOutMessage(ModbusDataReadOutMessage msg) {
528         boolean parse = true;
529
530         logger.debug("Received data read out message");
531         if (configuration.throttleTime > 0) {
532             if ((lastUpdateTime + configuration.throttleTime) > System.currentTimeMillis()) {
533                 logger.debug("Skipping data read out message parsing");
534                 parse = false;
535             }
536         }
537
538         if (parse) {
539             logger.debug("Parsing data read out message");
540             lastUpdateTime = System.currentTimeMillis();
541             List<ModbusValue> regValues = msg.getValues();
542
543             if (regValues != null) {
544                 for (ModbusValue val : regValues) {
545                     handleVariableUpdate(pumpModel, val);
546                 }
547             }
548         }
549     }
550
551     private void handleVariableUpdate(PumpModel pumpModel, ModbusValue value) {
552         logger.debug("Received variable update: {}", value);
553         int coilAddress = value.getCoilAddress();
554
555         VariableInformation variableInfo = VariableInformation.getVariableInfo(pumpModel, coilAddress);
556
557         if (variableInfo != null) {
558             logger.trace("Using variable information to register {}: {}", coilAddress, variableInfo);
559
560             int val = value.getValue();
561             logger.debug("{} = {}", coilAddress + ":" + variableInfo.variable + "/" + variableInfo.factor, val);
562
563             CacheObject oldValue = stateMap.get(coilAddress);
564
565             if (oldValue != null && val == oldValue.value
566                     && (oldValue.lastUpdateTime + refreshIntervalMillis() / 2) >= System.currentTimeMillis()) {
567                 logger.trace("Value did not change, ignoring update");
568             } else {
569                 final String channelPrefix = (variableInfo.type == Type.SETTING ? "setting#" : "sensor#");
570                 final String channelId = channelPrefix + String.valueOf(coilAddress);
571                 final String acceptedItemType = thing.getChannel(channelId).getAcceptedItemType();
572
573                 logger.trace("AcceptedItemType for channel {} = {}", channelId, acceptedItemType);
574                 State state = convertNibeValueToState(variableInfo, val, acceptedItemType);
575                 logger.debug("Setting state {} = {}", coilAddress + ":" + variableInfo.variable, state);
576                 stateMap.put(coilAddress, new CacheObject(System.currentTimeMillis(), val));
577                 updateState(new ChannelUID(getThing().getUID(), channelId), state);
578             }
579         } else {
580             logger.debug("Unknown register {}", coilAddress);
581         }
582     }
583
584     protected class CacheObject {
585
586         /** Time when cache object updated in milliseconds */
587         final long lastUpdateTime;
588
589         /** Cache value */
590         final int value;
591
592         /**
593          * Initialize cache object.
594          *
595          * @param lastUpdateTime Time in milliseconds.
596          * @param value Cache value.
597          */
598         CacheObject(long lastUpdateTime, int value) {
599             this.lastUpdateTime = lastUpdateTime;
600             this.value = value;
601         }
602     }
603 }