]> git.basschouten.com Git - openhab-addons.git/blob
b1f2860e85f0a6ebef0cf9b216ff9127c0f94fc5
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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.valloxmv.internal;
14
15 import java.io.ByteArrayOutputStream;
16 import java.io.IOException;
17 import java.io.InputStream;
18 import java.math.BigDecimal;
19 import java.net.URI;
20 import java.net.URISyntaxException;
21 import java.nio.ByteBuffer;
22 import java.time.ZonedDateTime;
23 import java.util.Calendar;
24 import java.util.HashMap;
25 import java.util.Map;
26 import java.util.TimeZone;
27 import java.util.concurrent.CountDownLatch;
28 import java.util.concurrent.Future;
29 import java.util.concurrent.TimeUnit;
30
31 import org.eclipse.jetty.websocket.api.Session;
32 import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
33 import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
34 import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
35 import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
36 import org.eclipse.jetty.websocket.api.annotations.WebSocket;
37 import org.eclipse.jetty.websocket.client.ClientUpgradeRequest;
38 import org.eclipse.jetty.websocket.client.WebSocketClient;
39 import org.openhab.core.library.types.DateTimeType;
40 import org.openhab.core.library.types.DecimalType;
41 import org.openhab.core.library.types.OnOffType;
42 import org.openhab.core.library.types.QuantityType;
43 import org.openhab.core.library.unit.MetricPrefix;
44 import org.openhab.core.library.unit.SIUnits;
45 import org.openhab.core.library.unit.Units;
46 import org.openhab.core.thing.ChannelUID;
47 import org.openhab.core.thing.ThingStatus;
48 import org.openhab.core.thing.ThingStatusDetail;
49 import org.openhab.core.types.State;
50 import org.slf4j.Logger;
51 import org.slf4j.LoggerFactory;
52
53 /**
54  * The {@link ValloxMVWebSocket} is responsible for socket communication with the vallox ventilation unit
55  *
56  * @author Björn Brings - Initial contribution
57  */
58 public class ValloxMVWebSocket {
59     private final ValloxMVHandler voHandler;
60     private final WebSocketClient client;
61     private final URI destUri;
62     private ValloxMVWebSocketListener socket;
63
64     private int iBoostTime;
65     private OnOffType ooBoostTimerEnabled;
66     private int iFireplaceTime;
67     private OnOffType ooFireplaceTimerEnabled;
68
69     private final Logger logger = LoggerFactory.getLogger(ValloxMVWebSocket.class);
70
71     public ValloxMVWebSocket(WebSocketClient webSocketClient, ValloxMVHandler voHandler, String ip) {
72         this.voHandler = voHandler;
73         this.client = webSocketClient;
74         URI tempUri;
75         try {
76             tempUri = new URI("ws://" + ip + ":80");
77         } catch (URISyntaxException e) {
78             tempUri = null;
79             connectionError(e);
80         }
81         destUri = tempUri;
82     }
83
84     public void request(ChannelUID channelUID, String updateState) {
85         Future<?> sessionFuture = null;
86         try {
87             socket = new ValloxMVWebSocketListener(channelUID, updateState);
88
89             ClientUpgradeRequest request = new ClientUpgradeRequest();
90             logger.debug("Connecting to: {}", destUri);
91             sessionFuture = client.connect(socket, destUri, request);
92             socket.awaitClose(2, TimeUnit.SECONDS);
93         } catch (InterruptedException | IOException e) {
94             connectionError(e);
95         } catch (Exception e) {
96             logger.debug("Unexpected error");
97             connectionError(e);
98         } finally {
99             if (sessionFuture != null && !sessionFuture.isDone()) {
100                 sessionFuture.cancel(true);
101             }
102         }
103     }
104
105     public void connectionError(Exception e) {
106         logger.debug("Error connecting vallox unit.", e);
107         voHandler.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR);
108     }
109
110     @WebSocket
111     public class ValloxMVWebSocketListener {
112         private final CountDownLatch closeLatch = new CountDownLatch(1);
113
114         private final Logger logger = LoggerFactory.getLogger(ValloxMVWebSocketListener.class);
115         private final String updateState;
116         private ChannelUID channelUID;
117         private int iValloxCmd;
118
119         public ValloxMVWebSocketListener(ChannelUID channelUID, String updateState) {
120             this.updateState = updateState;
121             this.channelUID = channelUID;
122         }
123
124         @OnWebSocketConnect
125         public void onConnect(Session session) {
126             try {
127                 logger.debug("Connected to: {}", session.getRemoteAddress().getAddress());
128                 ByteBuffer buf = generateRequest();
129                 session.getRemote().sendBytes(buf);
130             } catch (IOException e) {
131                 connectionError(e);
132             }
133         }
134
135         /**
136          * Method to generate ByteBuffer request to be sent to vallox online websocket
137          * to request or set data
138          *
139          * @param mode 246 for data request, 249 for setting data
140          * @param hmParameters HashMap for setting data with register as key and value as value
141          * @return ByteBuffer to be sent to websocket
142          */
143         public ByteBuffer generateCustomRequest(Integer mode, Map<Integer, Integer> hmParameters) {
144             // If we just want data the format is different so its just hardcoded here
145             if (mode == 246) {
146                 // requestData (Length 3, Command to get data 246, 0, checksum [sum of everything before])
147                 return ByteBuffer.wrap(new byte[] { 3, 0, (byte) 246, 0, 0, 0, (byte) 249, 0 });
148             }
149
150             int numberParameters = (hmParameters.size() * 2) + 2; // Parameters (key + value) + Mode + Checksum
151             int checksum = numberParameters;
152             // Allocate for bytebuffer incl. Length
153             ByteBuffer bb = ByteBuffer.allocate((numberParameters + 1) * 2)
154                     .put(convertIntegerIntoByteBuffer(numberParameters));
155
156             // Put Mode, HashMap and checksum to ByteArray
157             bb.put(convertIntegerIntoByteBuffer(mode));
158             checksum += mode;
159
160             for (Map.Entry<Integer, Integer> i : hmParameters.entrySet()) {
161                 bb.put(convertIntegerIntoByteBuffer(i.getKey()));
162                 bb.put(convertIntegerIntoByteBuffer(i.getValue()));
163                 checksum += i.getKey() + i.getValue();
164             }
165
166             // We have to make sure that checksum is within the range
167             // Checksum may hold larger value than 65535 from the above loop
168             // We will never reach integer max value in the loop so it is ok to do this after the loop
169             // This is not needed if we make sure that conversion to bytes will take care of it
170             // bitwise AND inside convertIntegerIntoByteBuffer() takes care of this
171             // checksum = checksum & 0xffff;
172
173             bb.put(convertIntegerIntoByteBuffer(checksum));
174             bb.position(0);
175             return bb;
176         }
177
178         // Convert Integer to ByteBuffer in Little-endian
179         public ByteBuffer convertIntegerIntoByteBuffer(Integer i) {
180             // Use bitwise operators to extract two rightmost bytes from the integer
181             byte b1 = (byte) (i & 0xff); // Rightmost byte
182             byte b2 = (byte) ((i >> 8) & 0xff); // Second rightmost byte
183             return ByteBuffer.wrap(new byte[] { b1, b2 });
184         }
185
186         public ByteBuffer generateRequest() {
187             if ((updateState == null) || (channelUID == null)) {
188                 // requestData (Length 3, Command to get data 246, empty set, checksum [sum of everything before])
189                 iValloxCmd = 246;
190                 return generateCustomRequest(246, new HashMap<>());
191             }
192             String strChannelUIDid = channelUID.getId();
193             int iUpdateState = Integer.parseInt(updateState);
194             Map<Integer, Integer> request = new HashMap<>();
195             switch (strChannelUIDid) {
196                 case ValloxMVBindingConstants.CHANNEL_STATE:
197                     switch (iUpdateState) {
198                         case ValloxMVBindingConstants.STATE_FIREPLACE:
199                             // Fireplace (Length 6, Command to set data 249, CYC_BOOST_TIMER (4612) = 0,
200                             // CYC_FIREPLACE_TIMER (4613) = value from CYC_FIREPLACE_TIME, checksum)
201                             // CYC_FIREPLACE_TIME is read during READ_TABLES and stored into outer class variable
202                             if (iFireplaceTime < 1) {
203                                 // use 15 minutes in case not initialized (should never happen)
204                                 iFireplaceTime = 15;
205                             }
206                             if (OnOffType.ON.equals(ooFireplaceTimerEnabled)) {
207                                 logger.debug("Changing to Fireplace profile, timer {} minutes", iFireplaceTime);
208                             } else {
209                                 logger.debug("Changing to Fireplace profile, timer not enabled");
210                             }
211                             request.put(4612, 0);
212                             request.put(4613, iFireplaceTime);
213                             break;
214                         case ValloxMVBindingConstants.STATE_ATHOME:
215                             // At Home (Length 8, Command to set data 249, CYC_STATE (4609) = 0,
216                             // CYC_BOOST_TIMER (4612) = 0, CYC_FIREPLACE_TIMER (4613) = 0, checksum)
217                             logger.debug("Changing to At Home profile");
218                             request.put(4609, 0);
219                             request.put(4612, 0);
220                             request.put(4613, 0);
221                             break;
222                         case ValloxMVBindingConstants.STATE_AWAY:
223                             // Away (Length 8, Command to set data 249, CYC_STATE (4609) = 1,
224                             // CYC_BOOST_TIMER (4612) = 0, CYC_FIREPLACE_TIMER (4613) = 0, checksum)
225                             logger.debug("Changing to Away profile");
226                             request.put(4609, 1);
227                             request.put(4612, 0);
228                             request.put(4613, 0);
229                             break;
230                         case ValloxMVBindingConstants.STATE_BOOST:
231                             // Boost (Length 6, Command to set data 249,
232                             // CYC_BOOST_TIMER (4612) = value from CYC_BOOST_TIME,
233                             // CYC_FIREPLACE_TIMER (4613) = 0, checksum)
234                             // CYC_BOOST_TIME is read during READ_TABLES and stored into outer class variable
235                             if (iBoostTime < 1) {
236                                 // use 30 minutes in case not initialized (should never happen)
237                                 iBoostTime = 30;
238                             }
239                             if (OnOffType.ON.equals(ooBoostTimerEnabled)) {
240                                 logger.debug("Changing to Boost profile, timer {} minutes", iBoostTime);
241                             } else {
242                                 logger.debug("Changing to Boost profile, timer not enabled");
243                             }
244                             request.put(4612, iBoostTime);
245                             request.put(4613, 0);
246                             break;
247                         default:
248                             // This should never happen. Let's get back to basic profile.
249                             // Clearing boost and fireplace timers.
250                             logger.debug("Incorrect profile requested, changing back to basic profile");
251                             request.put(4612, 0);
252                             request.put(4613, 0);
253                     }
254                     break;
255                 case ValloxMVBindingConstants.CHANNEL_ONOFF:
256                     request.put(4610, iUpdateState);
257                     break;
258                 case ValloxMVBindingConstants.CHANNEL_EXTR_FAN_BALANCE_BASE:
259                     request.put(20485, iUpdateState);
260                     break;
261                 case ValloxMVBindingConstants.CHANNEL_SUPP_FAN_BALANCE_BASE:
262                     request.put(20486, iUpdateState);
263                     break;
264                 case ValloxMVBindingConstants.CHANNEL_HOME_SPEED_SETTING:
265                     request.put(20507, iUpdateState);
266                     break;
267                 case ValloxMVBindingConstants.CHANNEL_AWAY_SPEED_SETTING:
268                     request.put(20501, iUpdateState);
269                     break;
270                 case ValloxMVBindingConstants.CHANNEL_BOOST_SPEED_SETTING:
271                     request.put(20513, iUpdateState);
272                     break;
273                 case ValloxMVBindingConstants.CHANNEL_HOME_AIR_TEMP_TARGET:
274                     request.put(20508, iUpdateState);
275                     break;
276                 case ValloxMVBindingConstants.CHANNEL_AWAY_AIR_TEMP_TARGET:
277                     request.put(20502, iUpdateState);
278                     break;
279                 case ValloxMVBindingConstants.CHANNEL_BOOST_AIR_TEMP_TARGET:
280                     request.put(20514, iUpdateState);
281                     break;
282                 case ValloxMVBindingConstants.CHANNEL_BOOST_TIME:
283                     iBoostTime = iUpdateState;
284                     request.put(20544, iUpdateState);
285                     break;
286                 case ValloxMVBindingConstants.CHANNEL_BOOST_TIMER_ENABLED:
287                     ooBoostTimerEnabled = OnOffType.from(Integer.toString(iUpdateState));
288                     request.put(21766, iUpdateState);
289                     break;
290                 case ValloxMVBindingConstants.CHANNEL_FIREPLACE_EXTR_FAN:
291                     request.put(20487, iUpdateState);
292                     break;
293                 case ValloxMVBindingConstants.CHANNEL_FIREPLACE_SUPP_FAN:
294                     request.put(20488, iUpdateState);
295                     break;
296                 case ValloxMVBindingConstants.CHANNEL_FIREPLACE_TIME:
297                     iFireplaceTime = iUpdateState;
298                     request.put(20545, iUpdateState);
299                     break;
300                 case ValloxMVBindingConstants.CHANNEL_FIREPLACE_TIMER_ENABLED:
301                     ooFireplaceTimerEnabled = OnOffType.from(Integer.toString(iUpdateState));
302                     request.put(21767, iUpdateState);
303                     break;
304                 case ValloxMVBindingConstants.CHANNEL_EXTRA_AIR_TEMP_TARGET:
305                     request.put(20493, iUpdateState);
306                     break;
307                 case ValloxMVBindingConstants.CHANNEL_EXTRA_EXTR_FAN:
308                     request.put(20494, iUpdateState);
309                     break;
310                 case ValloxMVBindingConstants.CHANNEL_EXTRA_SUPP_FAN:
311                     request.put(20495, iUpdateState);
312                     break;
313                 case ValloxMVBindingConstants.CHANNEL_EXTRA_TIME:
314                     request.put(20496, iUpdateState);
315                     break;
316                 case ValloxMVBindingConstants.CHANNEL_EXTRA_TIMER_ENABLED:
317                     request.put(21772, iUpdateState);
318                     break;
319                 case ValloxMVBindingConstants.CHANNEL_WEEKLY_TIMER_ENABLED:
320                     request.put(4615, iUpdateState);
321                     break;
322                 default:
323                     return null;
324             }
325             iValloxCmd = 249;
326             return generateCustomRequest(249, request);
327         }
328
329         @OnWebSocketMessage
330         public void onMessage(String message) {
331             logger.debug("Message from Server: {}", message);
332         }
333
334         @OnWebSocketError
335         public void onError(Throwable cause) {
336             voHandler.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
337             logger.debug("Connection failed: {}", cause.getMessage());
338         }
339
340         @OnWebSocketMessage
341         public void onBinary(InputStream in) {
342             logger.debug("Got binary message");
343             try {
344                 ByteArrayOutputStream buffer = new ByteArrayOutputStream();
345
346                 int nRead;
347                 byte[] data = new byte[16384];
348
349                 while ((nRead = in.read(data, 0, data.length)) != -1) {
350                     buffer.write(data, 0, nRead);
351                 }
352
353                 buffer.flush();
354
355                 byte[] bytes = buffer.toByteArray();
356
357                 if ((bytes.length > 5) && (bytes.length % 2 == 0)) {
358                     logger.debug("Response length: {} bytes", bytes.length);
359                 } else {
360                     logger.debug("Response corrupted, length: {} bytes", bytes.length);
361                     return;
362                 }
363
364                 int iDataLength = bytes.length / 2;
365
366                 // Verify responses to requests
367                 if ((iValloxCmd == 249) || (iValloxCmd == 250)) {
368                     // COMMAND_WRITE_DATA (249) or COMMAND_READ_DATA (250)
369                     int iChecksum = 0;
370                     int[] arriData = new int[iDataLength];
371                     for (int i = 0; i < iDataLength; i++) {
372                         arriData[i] = getNumberLE(bytes, (i * 2));
373                     }
374                     for (int i = 0; i < (iDataLength - 1); i++) {
375                         iChecksum += arriData[i];
376                     }
377                     iChecksum &= 0xffff;
378                     if ((arriData[0] != (iDataLength - 1)) || (arriData[iDataLength - 1] != iChecksum)) {
379                         // Data length or Checksum do not match
380                         logger.debug("Response corrupted, Data length or Checksum do not match");
381                         return;
382                     }
383                     // COMMAND_WRITE_DATA (249)
384                     if (iValloxCmd == 249) {
385                         String strChannelUIDid = channelUID.getId();
386                         if (arriData[1] == 245) {
387                             // ACK
388                             logger.debug("Channel {} successfully updated to {}", strChannelUIDid, updateState);
389                         } else {
390                             logger.debug("Channel {} could not be updated", strChannelUIDid);
391                         }
392                         return;
393                     }
394                     // COMMAND_READ_DATA (250)
395                     /* Read data command is not implemented, response check is ready for future implementation
396                     // @formatter:off
397                     if ((((iDataLength - 1) % 2) != 0) || (iDataLength < 5)) {
398                         logger.debug("Response corrupted, data length is wrong");
399                         return;
400                     }
401                     // number of data pairs (address, value) = (iDataLength - 3) / 2
402                     // First data pair (address, value) is in positions 2 and 3
403                     // (address, value) pairs could be put into array or hashmap
404                     // @formatter:on
405                     */
406                     logger.debug("Vallox command {} not implemented", iValloxCmd);
407                     return;
408                 } else if (iValloxCmd == 246) {
409                     // COMMAND_READ_TABLES (246)
410                     if (iDataLength > 704) {
411                         logger.debug("Data table response with {} values, updating to channels", iDataLength);
412                     } else {
413                         logger.debug("Response corrupted, data table response not complete");
414                         return;
415                     }
416                 } else {
417                     logger.debug("Vallox command {} not implemented", iValloxCmd);
418                     return;
419                 }
420
421                 // COMMAND_READ_TABLES (246)
422                 // Read values from received tables
423                 int iFanspeed = getNumberBE(bytes, 128);
424                 int iFanspeedExtract = getNumberBE(bytes, 144);
425                 int iFanspeedSupply = getNumberBE(bytes, 146);
426                 BigDecimal bdTempInside = getTemperature(bytes, 130);
427                 BigDecimal bdTempExhaust = getTemperature(bytes, 132);
428                 BigDecimal bdTempOutside = getTemperature(bytes, 134);
429                 BigDecimal bdTempIncomingBeforeHeating = getTemperature(bytes, 136);
430                 BigDecimal bdTempIncoming = getTemperature(bytes, 138);
431
432                 int iHumidity = getNumberBE(bytes, 148);
433                 @SuppressWarnings("unused")
434                 int iCo2 = getNumberBE(bytes, 150);
435
436                 int iStateOrig = getNumberBE(bytes, 214);
437                 int iBoostTimer = getNumberBE(bytes, 220);
438                 int iFireplaceTimer = getNumberBE(bytes, 222);
439
440                 int iCellstate = getNumberBE(bytes, 228);
441                 int iUptimeYears = getNumberBE(bytes, 230);
442                 int iUptimeHours = getNumberBE(bytes, 232);
443                 int iUptimeHoursCurrent = getNumberBE(bytes, 234);
444
445                 int iRemainingTimeForFilter = getNumberBE(bytes, 236);
446                 int iFilterChangedDateDay = getNumberBE(bytes, 496);
447                 int iFilterChangedDateMonth = getNumberBE(bytes, 498);
448                 int iFilterChangedDateYear = getNumberBE(bytes, 500);
449
450                 Calendar cFilterChangedDate = Calendar.getInstance();
451                 cFilterChangedDate.set(iFilterChangedDateYear + 2000,
452                         iFilterChangedDateMonth - 1 /* Month is 0-based */, iFilterChangedDateDay, 0, 0, 0);
453
454                 int iExtrFanBalanceBase = getNumberBE(bytes, 374);
455                 int iSuppFanBalanceBase = getNumberBE(bytes, 376);
456
457                 int iHomeSpeedSetting = getNumberBE(bytes, 418);
458                 int iAwaySpeedSetting = getNumberBE(bytes, 406);
459                 int iBoostSpeedSetting = getNumberBE(bytes, 430);
460                 BigDecimal bdHomeAirTempTarget = getTemperature(bytes, 420);
461                 BigDecimal bdAwayAirTempTarget = getTemperature(bytes, 408);
462                 BigDecimal bdBoostAirTempTarget = getTemperature(bytes, 432);
463
464                 // Using outer class variable for boost time and timer enabled
465                 iBoostTime = getNumberBE(bytes, 492);
466                 ooBoostTimerEnabled = OnOffType.from(Integer.toString(getNumberBE(bytes, 528)));
467                 int iFireplaceExtrFan = getNumberBE(bytes, 378);
468                 int iFireplaceSuppFan = getNumberBE(bytes, 380);
469                 // Using outer class variable for fireplace time and timer enabled
470                 iFireplaceTime = getNumberBE(bytes, 494);
471                 ooFireplaceTimerEnabled = OnOffType.from(Integer.toString(getNumberBE(bytes, 530)));
472                 BigDecimal bdExtraAirTempTarget = getTemperature(bytes, 390);
473                 int iExtraExtrFan = getNumberBE(bytes, 392);
474                 int iExtraSuppFan = getNumberBE(bytes, 394);
475                 int iExtraTime = getNumberBE(bytes, 396);
476                 OnOffType ooExtraTimerEnabled = OnOffType.from(Integer.toString(getNumberBE(bytes, 540)));
477                 OnOffType ooWeeklyTimerEnabled = OnOffType.from(Integer.toString(getNumberBE(bytes, 226)));
478
479                 BigDecimal bdState;
480                 if (iFireplaceTimer > 0) {
481                     bdState = new BigDecimal(ValloxMVBindingConstants.STATE_FIREPLACE);
482                 } else if (iBoostTimer > 0) {
483                     bdState = new BigDecimal(ValloxMVBindingConstants.STATE_BOOST);
484                 } else if (iStateOrig == 1) {
485                     bdState = new BigDecimal(ValloxMVBindingConstants.STATE_AWAY);
486                 } else {
487                     bdState = new BigDecimal(ValloxMVBindingConstants.STATE_ATHOME);
488                 }
489
490                 OnOffType ooOnOff = OnOffType.from(bytes[217] != 5);
491
492                 // Update channels with read values
493                 updateChannel(ValloxMVBindingConstants.CHANNEL_ONOFF, ooOnOff);
494                 updateChannel(ValloxMVBindingConstants.CHANNEL_STATE, new DecimalType(bdState));
495                 updateChannel(ValloxMVBindingConstants.CHANNEL_FAN_SPEED, new QuantityType<>(iFanspeed, Units.PERCENT));
496                 updateChannel(ValloxMVBindingConstants.CHANNEL_FAN_SPEED_EXTRACT, new DecimalType(iFanspeedExtract));
497                 updateChannel(ValloxMVBindingConstants.CHANNEL_FAN_SPEED_SUPPLY, new DecimalType(iFanspeedSupply));
498                 updateChannel(ValloxMVBindingConstants.CHANNEL_TEMPERATURE_INSIDE,
499                         new QuantityType<>(bdTempInside, SIUnits.CELSIUS));
500                 updateChannel(ValloxMVBindingConstants.CHANNEL_TEMPERATURE_OUTSIDE,
501                         new QuantityType<>(bdTempOutside, SIUnits.CELSIUS));
502                 updateChannel(ValloxMVBindingConstants.CHANNEL_TEMPERATURE_EXHAUST,
503                         new QuantityType<>(bdTempExhaust, SIUnits.CELSIUS));
504                 updateChannel(ValloxMVBindingConstants.CHANNEL_TEMPERATURE_INCOMING_BEFORE_HEATING,
505                         new QuantityType<>(bdTempIncomingBeforeHeating, SIUnits.CELSIUS));
506                 updateChannel(ValloxMVBindingConstants.CHANNEL_TEMPERATURE_INCOMING,
507                         new QuantityType<>(bdTempIncoming, SIUnits.CELSIUS));
508                 updateChannel(ValloxMVBindingConstants.CHANNEL_HUMIDITY, new QuantityType<>(iHumidity, Units.PERCENT));
509                 updateChannel(ValloxMVBindingConstants.CHANNEL_CO2, new QuantityType<>(iCo2, Units.PARTS_PER_MILLION));
510                 updateChannel(ValloxMVBindingConstants.CHANNEL_CELLSTATE, new DecimalType(iCellstate));
511                 updateChannel(ValloxMVBindingConstants.CHANNEL_UPTIME_YEARS, new DecimalType(iUptimeYears));
512                 updateChannel(ValloxMVBindingConstants.CHANNEL_UPTIME_HOURS, new DecimalType(iUptimeHours));
513                 updateChannel(ValloxMVBindingConstants.CHANNEL_UPTIME_HOURS_CURRENT,
514                         new DecimalType(iUptimeHoursCurrent));
515                 updateChannel(ValloxMVBindingConstants.CHANNEL_FILTER_CHANGED_DATE, new DateTimeType(
516                         ZonedDateTime.ofInstant(cFilterChangedDate.toInstant(), TimeZone.getDefault().toZoneId())));
517                 updateChannel(ValloxMVBindingConstants.CHANNEL_REMAINING_FILTER_DAYS,
518                         new QuantityType<>(iRemainingTimeForFilter, Units.DAY));
519                 updateChannel(ValloxMVBindingConstants.CHANNEL_EXTR_FAN_BALANCE_BASE,
520                         new QuantityType<>(iExtrFanBalanceBase, Units.PERCENT));
521                 updateChannel(ValloxMVBindingConstants.CHANNEL_SUPP_FAN_BALANCE_BASE,
522                         new QuantityType<>(iSuppFanBalanceBase, Units.PERCENT));
523                 updateChannel(ValloxMVBindingConstants.CHANNEL_HOME_SPEED_SETTING,
524                         new QuantityType<>(iHomeSpeedSetting, Units.PERCENT));
525                 updateChannel(ValloxMVBindingConstants.CHANNEL_AWAY_SPEED_SETTING,
526                         new QuantityType<>(iAwaySpeedSetting, Units.PERCENT));
527                 updateChannel(ValloxMVBindingConstants.CHANNEL_BOOST_SPEED_SETTING,
528                         new QuantityType<>(iBoostSpeedSetting, Units.PERCENT));
529                 updateChannel(ValloxMVBindingConstants.CHANNEL_HOME_AIR_TEMP_TARGET,
530                         new QuantityType<>(bdHomeAirTempTarget, SIUnits.CELSIUS));
531                 updateChannel(ValloxMVBindingConstants.CHANNEL_AWAY_AIR_TEMP_TARGET,
532                         new QuantityType<>(bdAwayAirTempTarget, SIUnits.CELSIUS));
533                 updateChannel(ValloxMVBindingConstants.CHANNEL_BOOST_AIR_TEMP_TARGET,
534                         new QuantityType<>(bdBoostAirTempTarget, SIUnits.CELSIUS));
535                 updateChannel(ValloxMVBindingConstants.CHANNEL_BOOST_TIME, new DecimalType(iBoostTime));
536                 updateChannel(ValloxMVBindingConstants.CHANNEL_BOOST_TIMER_ENABLED, ooBoostTimerEnabled);
537                 updateChannel(ValloxMVBindingConstants.CHANNEL_FIREPLACE_EXTR_FAN,
538                         new QuantityType<>(iFireplaceExtrFan, Units.PERCENT));
539                 updateChannel(ValloxMVBindingConstants.CHANNEL_FIREPLACE_SUPP_FAN,
540                         new QuantityType<>(iFireplaceSuppFan, Units.PERCENT));
541                 updateChannel(ValloxMVBindingConstants.CHANNEL_FIREPLACE_TIME, new DecimalType(iFireplaceTime));
542                 updateChannel(ValloxMVBindingConstants.CHANNEL_FIREPLACE_TIMER_ENABLED, ooFireplaceTimerEnabled);
543                 updateChannel(ValloxMVBindingConstants.CHANNEL_EXTRA_AIR_TEMP_TARGET,
544                         new QuantityType<>(bdExtraAirTempTarget, SIUnits.CELSIUS));
545                 updateChannel(ValloxMVBindingConstants.CHANNEL_EXTRA_EXTR_FAN,
546                         new QuantityType<>(iExtraExtrFan, Units.PERCENT));
547                 updateChannel(ValloxMVBindingConstants.CHANNEL_EXTRA_SUPP_FAN,
548                         new QuantityType<>(iExtraSuppFan, Units.PERCENT));
549                 updateChannel(ValloxMVBindingConstants.CHANNEL_EXTRA_TIME, new DecimalType(iExtraTime));
550                 updateChannel(ValloxMVBindingConstants.CHANNEL_EXTRA_TIMER_ENABLED, ooExtraTimerEnabled);
551                 updateChannel(ValloxMVBindingConstants.CHANNEL_WEEKLY_TIMER_ENABLED, ooWeeklyTimerEnabled);
552
553                 voHandler.updateStatus(ThingStatus.ONLINE);
554                 logger.debug("Data updated successfully");
555
556             } catch (IOException e) {
557                 connectionError(e);
558             }
559         }
560
561         private void updateChannel(String strChannelName, State state) {
562             voHandler.updateState(strChannelName, state);
563         }
564
565         private int getNumberBE(byte[] bytes, int pos) {
566             return ((bytes[pos] & 0xff) << 8) | (bytes[pos + 1] & 0xff);
567         }
568
569         private int getNumberLE(byte[] bytes, int pos) {
570             return (bytes[pos] & 0xff) | ((bytes[pos + 1] & 0xff) << 8);
571         }
572
573         @SuppressWarnings("null")
574         private BigDecimal getTemperature(byte[] bytes, int pos) {
575             // Fetch 2 byte number out of bytearray representing the temperature in centiKelvin
576             BigDecimal bdTemperatureCentiKelvin = new BigDecimal(getNumberBE(bytes, pos));
577             // Return number converted to degree celsius (= (centiKelvin - 27315) / 100 )
578             return (new QuantityType<>(bdTemperatureCentiKelvin, MetricPrefix.CENTI(Units.KELVIN))
579                     .toUnit(SIUnits.CELSIUS)).toBigDecimal();
580         }
581
582         @OnWebSocketClose
583         public void onClose(int statusCode, String reason) {
584             logger.debug("WebSocket Closed. Code: {}; Reason: {}", statusCode, reason);
585             this.closeLatch.countDown();
586         }
587
588         public boolean awaitClose(int duration, TimeUnit unit) throws InterruptedException {
589             return this.closeLatch.await(duration, unit);
590         }
591     }
592 }