]> git.basschouten.com Git - openhab-addons.git/blob
e8169113b7beef79cc78d6364be500a82f30a8c3
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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.bluetooth.daikinmadoka.handler;
14
15 import java.util.Arrays;
16 import java.util.Random;
17 import java.util.concurrent.Executor;
18 import java.util.concurrent.ExecutorService;
19 import java.util.concurrent.Executors;
20 import java.util.concurrent.ScheduledFuture;
21 import java.util.concurrent.TimeUnit;
22
23 import javax.measure.quantity.Temperature;
24 import javax.measure.quantity.Time;
25
26 import org.eclipse.jdt.annotation.NonNull;
27 import org.eclipse.jdt.annotation.NonNullByDefault;
28 import org.eclipse.jdt.annotation.Nullable;
29 import org.openhab.binding.bluetooth.BluetoothCharacteristic;
30 import org.openhab.binding.bluetooth.BluetoothCompletionStatus;
31 import org.openhab.binding.bluetooth.BluetoothDevice.ConnectionState;
32 import org.openhab.binding.bluetooth.ConnectedBluetoothHandler;
33 import org.openhab.binding.bluetooth.daikinmadoka.DaikinMadokaBindingConstants;
34 import org.openhab.binding.bluetooth.daikinmadoka.internal.BRC1HUartProcessor;
35 import org.openhab.binding.bluetooth.daikinmadoka.internal.DaikinMadokaConfiguration;
36 import org.openhab.binding.bluetooth.daikinmadoka.internal.model.MadokaMessage;
37 import org.openhab.binding.bluetooth.daikinmadoka.internal.model.MadokaParsingException;
38 import org.openhab.binding.bluetooth.daikinmadoka.internal.model.MadokaProperties.FanSpeed;
39 import org.openhab.binding.bluetooth.daikinmadoka.internal.model.MadokaProperties.OperationMode;
40 import org.openhab.binding.bluetooth.daikinmadoka.internal.model.MadokaSettings;
41 import org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands.BRC1HCommand;
42 import org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands.DisableCleanFilterIndicatorCommand;
43 import org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands.EnterPrivilegedModeCommand;
44 import org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands.GetCleanFilterIndicatorCommand;
45 import org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands.GetEyeBrightnessCommand;
46 import org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands.GetFanspeedCommand;
47 import org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands.GetIndoorOutoorTemperatures;
48 import org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands.GetOperationHoursCommand;
49 import org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands.GetOperationmodeCommand;
50 import org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands.GetPowerstateCommand;
51 import org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands.GetSetpointCommand;
52 import org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands.GetVersionCommand;
53 import org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands.ResetCleanFilterTimerCommand;
54 import org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands.ResponseListener;
55 import org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands.SetEyeBrightnessCommand;
56 import org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands.SetFanspeedCommand;
57 import org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands.SetOperationmodeCommand;
58 import org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands.SetPowerstateCommand;
59 import org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands.SetSetpointCommand;
60 import org.openhab.core.common.NamedThreadFactory;
61 import org.openhab.core.library.types.DecimalType;
62 import org.openhab.core.library.types.OnOffType;
63 import org.openhab.core.library.types.PercentType;
64 import org.openhab.core.library.types.QuantityType;
65 import org.openhab.core.library.types.StringType;
66 import org.openhab.core.thing.ChannelUID;
67 import org.openhab.core.thing.Thing;
68 import org.openhab.core.types.Command;
69 import org.openhab.core.types.RefreshType;
70 import org.openhab.core.types.State;
71 import org.openhab.core.types.UnDefType;
72 import org.openhab.core.util.HexUtils;
73 import org.slf4j.Logger;
74 import org.slf4j.LoggerFactory;
75
76 /**
77  * The {@link DaikinMadokaHandler} is responsible for handling commands, which are
78  * sent to one of the channels as well as updating channel values.
79  *
80  * @author Benjamin Lafois - Initial contribution
81  */
82 @NonNullByDefault
83 public class DaikinMadokaHandler extends ConnectedBluetoothHandler implements ResponseListener {
84
85     private final Logger logger = LoggerFactory.getLogger(DaikinMadokaHandler.class);
86
87     private @Nullable DaikinMadokaConfiguration config;
88
89     private @Nullable ExecutorService commandExecutor;
90
91     private @Nullable ScheduledFuture<?> refreshJob;
92
93     // UART Processor is in charge of reassembling chunks
94     private BRC1HUartProcessor uartProcessor = new BRC1HUartProcessor(this);
95
96     private volatile @Nullable BRC1HCommand currentCommand = null;
97
98     private MadokaSettings madokaSettings = new MadokaSettings();
99
100     public DaikinMadokaHandler(Thing thing) {
101         super(thing);
102     }
103
104     @Override
105     public void initialize() {
106         super.initialize();
107
108         logger.debug("[{}] Start initializing!", super.thing.getUID().getId());
109
110         // Load Configuration
111         config = getConfigAs(DaikinMadokaConfiguration.class);
112         DaikinMadokaConfiguration c = config;
113
114         logger.debug("[{}] Parameter value [refreshInterval]: {}", super.thing.getUID().getId(), c.refreshInterval);
115         logger.debug("[{}] Parameter value [commandTimeout]: {}", super.thing.getUID().getId(), c.commandTimeout);
116
117         if (getBridge() == null) {
118             logger.debug("[{}] Bridge is null. Exiting.", super.thing.getUID().getId());
119             return;
120         }
121
122         this.commandExecutor = Executors
123                 .newSingleThreadExecutor(new NamedThreadFactory(thing.getUID().getAsString(), true));
124
125         this.refreshJob = scheduler.scheduleWithFixedDelay(() -> {
126             // It is useless to refresh version all the time ! Just once.
127             if (this.madokaSettings.getCommunicationControllerVersion() == null
128                     || this.madokaSettings.getRemoteControllerVersion() == null) {
129                 submitCommand(new GetVersionCommand());
130             }
131             submitCommand(new GetIndoorOutoorTemperatures());
132             submitCommand(new GetOperationmodeCommand());
133             submitCommand(new GetPowerstateCommand()); // always keep the "GetPowerState" aftern the "GetOperationMode"
134             submitCommand(new GetSetpointCommand());
135             submitCommand(new GetFanspeedCommand());
136             submitCommand(new GetCleanFilterIndicatorCommand());
137
138             try {
139                 // As it is a complex operation - it has been extracted to a method.
140                 retrieveOperationHours();
141             } catch (InterruptedException e) {
142                 // The thread wants to exit!
143                 return;
144             }
145
146             submitCommand(new GetEyeBrightnessCommand());
147         }, new Random().nextInt(30), c.refreshInterval, TimeUnit.SECONDS); // We introduce a random start time, it
148                                                                            // avoids when having multiple devices to
149                                                                            // have the commands sent simultaneously.
150     }
151
152     private void retrieveOperationHours() throws InterruptedException {
153         // This one is special - and MUST be ran twice, after being in priv mode
154         // run it once an hour is sufficient... TODO
155         submitCommand(new EnterPrivilegedModeCommand());
156         submitCommand(new GetOperationHoursCommand());
157         // a 1second+ delay is necessary
158         Thread.sleep(1500);
159
160         submitCommand(new GetOperationHoursCommand());
161     }
162
163     @Override
164     public void dispose() {
165         logger.debug("[{}] dispose()", super.thing.getUID().getId());
166
167         dispose(refreshJob);
168         dispose(commandExecutor);
169         dispose(currentCommand);
170
171         // Unsubscribe to characteristic notifications
172         if (this.device != null) {
173             BluetoothCharacteristic charNotif = this.device
174                     .getCharacteristic(DaikinMadokaBindingConstants.CHAR_NOTIF_UUID);
175
176             if (charNotif != null) {
177                 @NonNull
178                 BluetoothCharacteristic c = charNotif;
179                 this.device.disableNotifications(c);
180             }
181         }
182
183         super.dispose();
184     }
185
186     private static void dispose(@Nullable ExecutorService executor) {
187         if (executor != null) {
188             executor.shutdownNow();
189         }
190     }
191
192     private static void dispose(@Nullable ScheduledFuture<?> future) {
193         if (future != null) {
194             future.cancel(true);
195         }
196     }
197
198     private static void dispose(@Nullable BRC1HCommand command) {
199         if (command != null) {
200             // even if it already completed it doesn't really matter.
201             // on the off chance that the commandExecutor is waiting on the command, we can wake it up and cause it to
202             // terminate
203             command.setState(BRC1HCommand.State.FAILED);
204         }
205     }
206
207     @Override
208     public void handleCommand(ChannelUID channelUID, Command command) {
209         logger.debug("[{}] Channel: {}, Command: {}", super.thing.getUID().getId(), channelUID, command);
210
211         if (command instanceof RefreshType) {
212             // The refresh commands are not supported in query mode.
213             // The binding will notify updates on channels
214             return;
215         }
216
217         switch (channelUID.getId()) {
218             case DaikinMadokaBindingConstants.CHANNEL_ID_CLEAN_FILTER_INDICATOR:
219                 OnOffType cleanFilterOrder = (OnOffType) command;
220                 if (cleanFilterOrder == OnOffType.OFF) {
221                     resetCleanFilterIndicator();
222                 }
223                 break;
224             case DaikinMadokaBindingConstants.CHANNEL_ID_SETPOINT:
225                 try {
226                     QuantityType<Temperature> setpoint = (QuantityType<Temperature>) command;
227                     submitCommand(new SetSetpointCommand(setpoint, setpoint));
228                 } catch (Exception e) {
229                     logger.warn("Data received is not a valid temperature.", e);
230                 }
231                 break;
232             case DaikinMadokaBindingConstants.CHANNEL_ID_EYE_BRIGHTNESS:
233                 try {
234                     logger.debug("Set eye brightness with value {}, {}", command.getClass().getName(), command);
235                     PercentType p = (PercentType) command;
236                     submitCommand(new SetEyeBrightnessCommand(p));
237                 } catch (Exception e) {
238                     logger.warn("Data received is not a valid Eye Brightness status", e);
239                 }
240                 break;
241             case DaikinMadokaBindingConstants.CHANNEL_ID_ONOFF_STATUS:
242                 try {
243                     OnOffType oot = (OnOffType) command;
244                     submitCommand(new SetPowerstateCommand(oot));
245                 } catch (Exception e) {
246                     logger.warn("Data received is not a valid on/off status", e);
247                 }
248                 break;
249             case DaikinMadokaBindingConstants.CHANNEL_ID_FAN_SPEED:
250                 try {
251                     DecimalType fanSpeed = (DecimalType) command;
252                     FanSpeed fs = FanSpeed.valueOf(fanSpeed.intValue());
253                     submitCommand(new SetFanspeedCommand(fs, fs));
254                 } catch (Exception e) {
255                     logger.warn("Data received is not a valid FanSpeed status", e);
256                 }
257                 break;
258             case DaikinMadokaBindingConstants.CHANNEL_ID_OPERATION_MODE:
259                 try {
260                     StringType operationMode = (StringType) command;
261                     OperationMode m = OperationMode.valueOf(operationMode.toFullString());
262
263                     submitCommand(new SetOperationmodeCommand(m));
264                 } catch (Exception e) {
265                     logger.warn("Data received is not a valid OPERATION MODE", e);
266                 }
267                 break;
268             case DaikinMadokaBindingConstants.CHANNEL_ID_HOMEBRIDGE_MODE:
269                 try {
270                     // Homebridge are discrete value different from Daikin
271                     // 0 - Off
272                     // 1 - Heating
273                     // 2 - Cooling
274                     // 3 - Auto
275                     DecimalType homebridgeMode = (DecimalType) command;
276                     switch (homebridgeMode.intValue()) {
277                         case 0: // Off
278                             submitCommand(new SetPowerstateCommand(OnOffType.OFF));
279                             break;
280                         case 1: // Heating
281                             submitCommand(new SetOperationmodeCommand(OperationMode.HEAT));
282                             if (madokaSettings.getOnOffState() == OnOffType.OFF) {
283                                 submitCommand(new SetPowerstateCommand(OnOffType.ON));
284                             }
285                             break;
286                         case 2: // Cooling
287                             submitCommand(new SetOperationmodeCommand(OperationMode.COOL));
288                             if (madokaSettings.getOnOffState() == OnOffType.OFF) {
289                                 submitCommand(new SetPowerstateCommand(OnOffType.ON));
290                             }
291                             break;
292                         case 3: // Auto
293                             submitCommand(new SetOperationmodeCommand(OperationMode.AUTO));
294                             if (madokaSettings.getOnOffState() == OnOffType.OFF) {
295                                 submitCommand(new SetPowerstateCommand(OnOffType.ON));
296                             }
297                             break;
298                         default: // Invalid Value - in case of new FW
299                             logger.warn("Invalid value received for channel {}. Ignoring.", channelUID);
300                             break;
301                     }
302                 } catch (Exception e) {
303                     logger.warn("Data received is not a valid HOMEBRIDGE OPERATION MODE", e);
304                 }
305                 break;
306             case DaikinMadokaBindingConstants.CHANNEL_ID_HOMEKIT_TARGET_HEATING_COOLING_MODE:
307                 try {
308                     StringType homekitOperationMode = (StringType) command;
309
310                     switch (homekitOperationMode.toString()) {
311                         case "Off":
312                             submitCommand(new SetPowerstateCommand(OnOffType.OFF));
313                             break;
314                         case "CoolOn":
315                             submitCommand(new SetOperationmodeCommand(OperationMode.COOL));
316                             if (madokaSettings.getOnOffState() == OnOffType.OFF) {
317                                 submitCommand(new SetPowerstateCommand(OnOffType.ON));
318                             }
319                             break;
320                         case "HeatOn":
321                             submitCommand(new SetOperationmodeCommand(OperationMode.HEAT));
322                             if (madokaSettings.getOnOffState() == OnOffType.OFF) {
323                                 submitCommand(new SetPowerstateCommand(OnOffType.ON));
324                             }
325                             break;
326                         case "Auto":
327                             submitCommand(new SetOperationmodeCommand(OperationMode.AUTO));
328                             if (madokaSettings.getOnOffState() == OnOffType.OFF) {
329                                 submitCommand(new SetPowerstateCommand(OnOffType.ON));
330                             }
331                             break;
332                         default:
333                             break;
334                     }
335                 } catch (Exception e) {
336                     logger.info("Error while setting mode through HomeKIt received Mode");
337                 }
338             default:
339                 break;
340         }
341     }
342
343     /**
344      * 2 actions need to be done: disable the notification AND reset the filter timer
345      */
346     private void resetCleanFilterIndicator() {
347         logger.debug("[{}] resetCleanFilterIndicator()", super.thing.getUID().getId());
348         submitCommand(new DisableCleanFilterIndicatorCommand());
349         submitCommand(new ResetCleanFilterTimerCommand());
350     }
351
352     @Override
353     public void onCharacteristicUpdate(BluetoothCharacteristic characteristic) {
354         if (logger.isDebugEnabled()) {
355             logger.debug("[{}] onCharacteristicUpdate({})", super.thing.getUID().getId(),
356                     HexUtils.bytesToHex(characteristic.getByteValue()));
357         }
358         super.onCharacteristicUpdate(characteristic);
359
360         // Check that arguments are valid.
361         if (characteristic.getUuid() == null) {
362             return;
363         }
364
365         // We are only interested in the Notify Characteristic of UART service
366         if (!characteristic.getUuid().equals(DaikinMadokaBindingConstants.CHAR_NOTIF_UUID)) {
367             return;
368         }
369
370         // A message cannot be null or have a 0-byte length
371         byte[] msgBytes = characteristic.getByteValue();
372         if (msgBytes == null || msgBytes.length == 0) {
373             return;
374         }
375
376         this.uartProcessor.chunkReceived(msgBytes);
377     }
378
379     private void submitCommand(BRC1HCommand command) {
380         Executor executor = commandExecutor;
381
382         if (executor != null) {
383             executor.execute(() -> processCommand(command));
384         }
385     }
386
387     private void processCommand(BRC1HCommand command) {
388         logger.debug("[{}] ProcessCommand {}", super.thing.getUID().getId(), command.getClass().getSimpleName());
389
390         try {
391             currentCommand = command;
392             uartProcessor.abandon();
393
394             if (device == null || device.getConnectionState() != ConnectionState.CONNECTED) {
395                 logger.debug("Unable to send command {} to device {}: not connected",
396                         command.getClass().getSimpleName(), address);
397                 command.setState(BRC1HCommand.State.FAILED);
398                 return;
399             }
400
401             if (!resolved) {
402                 logger.debug("Unable to send command {} to device {}: services not resolved",
403                         command.getClass().getSimpleName(), device.getAddress());
404                 command.setState(BRC1HCommand.State.FAILED);
405                 return;
406             }
407
408             BluetoothCharacteristic charWrite = device
409                     .getCharacteristic(DaikinMadokaBindingConstants.CHAR_WRITE_WITHOUT_RESPONSE_UUID);
410             if (charWrite == null) {
411                 logger.warn("Unable to execute {}. Characteristic '{}' could not be found.",
412                         command.getClass().getSimpleName(),
413                         DaikinMadokaBindingConstants.CHAR_WRITE_WITHOUT_RESPONSE_UUID);
414                 command.setState(BRC1HCommand.State.FAILED);
415                 return;
416             }
417
418             BluetoothCharacteristic charNotif = this.device
419                     .getCharacteristic(DaikinMadokaBindingConstants.CHAR_NOTIF_UUID);
420
421             if (charNotif != null) {
422                 device.enableNotifications(charNotif);
423             }
424
425             // Commands can be composed of multiple chunks
426             for (byte[] chunk : command.getRequest()) {
427                 charWrite.setValue(chunk);
428                 command.setState(BRC1HCommand.State.ENQUEUED);
429                 for (int i = 0; i < DaikinMadokaBindingConstants.WRITE_CHARACTERISTIC_MAX_RETRIES; i++) {
430                     if (device.writeCharacteristic(charWrite)) {
431                         command.setState(BRC1HCommand.State.SENT);
432                         synchronized (command) {
433                             command.wait(100);
434                         }
435                         break;
436                     }
437                     Thread.sleep(100);
438                 }
439             }
440
441             if (command.getState() == BRC1HCommand.State.SENT && this.config != null) {
442                 if (!command.awaitStateChange(this.config.commandTimeout, TimeUnit.MILLISECONDS,
443                         BRC1HCommand.State.SUCCEEDED, BRC1HCommand.State.FAILED)) {
444                     logger.debug("[{}] Command {} to device {} timed out", super.thing.getUID().getId(), command,
445                             device.getAddress());
446                     command.setState(BRC1HCommand.State.FAILED);
447                 }
448             }
449         } catch (Exception e) {
450             currentCommand = null;
451             // Let the exception bubble the stack!
452             throw new RuntimeException(e);
453         }
454
455         try {
456             Thread.sleep(200);
457         } catch (InterruptedException e) {
458             Thread.currentThread().interrupt();
459         }
460     }
461
462     @Override
463     public void onCharacteristicWriteComplete(BluetoothCharacteristic characteristic,
464             BluetoothCompletionStatus status) {
465         super.onCharacteristicWriteComplete(characteristic, status);
466
467         byte[] request = characteristic.getByteValue();
468         BRC1HCommand command = currentCommand;
469
470         if (command != null) {
471             // last chunk:
472             byte[] lastChunk = command.getRequest()[command.getRequest().length - 1];
473             if (!Arrays.equals(request, lastChunk)) {
474                 logger.debug("Write completed for a chunk, but not a complete command.");
475                 synchronized (command) {
476                     command.notify();
477                 }
478                 return;
479             }
480             switch (status) {
481                 case SUCCESS:
482                     command.setState(BRC1HCommand.State.SENT);
483                     break;
484                 case ERROR:
485                     command.setState(BRC1HCommand.State.FAILED);
486                     break;
487             }
488         } else {
489             if (logger.isDebugEnabled()) {
490                 logger.debug("No command found that matches request {}", HexUtils.bytesToHex(request));
491             }
492         }
493     }
494
495     /**
496      * When the method is triggered, it means that all message chunks have been received, re-assembled in the right
497      * order and that the payload is ready to be processed.
498      */
499     @Override
500     public void receivedResponse(byte[] response) {
501         logger.debug("Received Response");
502         BRC1HCommand command = currentCommand;
503
504         if (command == null) {
505             if (logger.isDebugEnabled()) {
506                 logger.debug("No command present to handle response {}", HexUtils.bytesToHex(response));
507             }
508         } else {
509             try {
510                 command.handleResponse(scheduler, this, MadokaMessage.parse(response));
511             } catch (MadokaParsingException e) {
512                 logger.debug("Response message could not be parsed correctly ({}): {}. Reason: {}",
513                         command.getClass().getSimpleName(), HexUtils.bytesToHex(response), e.getMessage());
514             }
515         }
516     }
517
518     @Override
519     public void receivedResponse(GetVersionCommand command) {
520         String commCtrlVers = command.getCommunicationControllerVersion();
521         if (commCtrlVers != null) {
522             this.madokaSettings.setCommunicationControllerVersion(commCtrlVers);
523             updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_COMMUNICATION_CONTROLLER_VERSION,
524                     new StringType(commCtrlVers));
525         }
526
527         String remoteCtrlVers = command.getRemoteControllerVersion();
528         if (remoteCtrlVers != null) {
529             this.madokaSettings.setRemoteControllerVersion(remoteCtrlVers);
530             updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_REMOTE_CONTROLLER_VERSION,
531                     new StringType(remoteCtrlVers));
532         }
533     }
534
535     @Override
536     public void receivedResponse(GetFanspeedCommand command) {
537         if (command.getCoolingFanSpeed() == null || command.getHeatingFanSpeed() == null) {
538             return;
539         }
540
541         // We need the current operation mode to determine which Fan Speed we use (cooling or heating)
542         OperationMode operationMode = this.madokaSettings.getOperationMode();
543         if (operationMode == null) {
544             return;
545         }
546
547         FanSpeed fs;
548
549         switch (operationMode) {
550             case AUTO:
551                 logger.debug("In AutoMode, CoolingFanSpeed = {}, HeatingFanSpeed = {}", command.getCoolingFanSpeed(),
552                         command.getHeatingFanSpeed());
553                 fs = command.getHeatingFanSpeed();
554                 break;
555             case HEAT:
556                 fs = command.getHeatingFanSpeed();
557                 break;
558             case COOL:
559                 fs = command.getCoolingFanSpeed();
560                 break;
561             default:
562                 return;
563         }
564
565         if (fs == null) {
566             return;
567         }
568
569         // No need to re-set if it is the same value
570         if (fs.equals(this.madokaSettings.getFanspeed())) {
571             return;
572         }
573
574         this.madokaSettings.setFanspeed(fs);
575         updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_FAN_SPEED, new DecimalType(fs.value()));
576     }
577
578     @Override
579     public void receivedResponse(GetSetpointCommand command) {
580         if (command.getCoolingSetpoint() == null || command.getHeatingSetpoint() == null) {
581             return;
582         }
583
584         // We need the current operation mode to determine which Fan Speed we use (cooling or heating)
585         OperationMode operationMode = this.madokaSettings.getOperationMode();
586         if (operationMode == null) {
587             return;
588         }
589
590         QuantityType<Temperature> sp;
591
592         switch (operationMode) {
593             case AUTO:
594                 logger.debug("In AutoMode, CoolingSetpoint = {}, HeatingSetpoint = {}", command.getCoolingSetpoint(),
595                         command.getHeatingSetpoint());
596                 sp = command.getHeatingSetpoint();
597                 break;
598             case HEAT:
599                 sp = command.getHeatingSetpoint();
600                 break;
601             case COOL:
602                 sp = command.getCoolingSetpoint();
603                 break;
604             default:
605                 return;
606         }
607
608         if (sp == null) {
609             return;
610         }
611
612         // No need to re-set if it is the same value
613         if (sp.equals(this.madokaSettings.getSetpoint())) {
614             return;
615         }
616
617         this.madokaSettings.setSetpoint(sp);
618
619         QuantityType<Temperature> dt = this.madokaSettings.getSetpoint();
620         if (dt != null) {
621             updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_SETPOINT, dt);
622         }
623     }
624
625     @Override
626     public void receivedResponse(GetOperationmodeCommand command) {
627         if (command.getOperationMode() == null) {
628             logger.debug("OperationMode is null.");
629             return;
630         }
631
632         OperationMode newMode = command.getOperationMode();
633
634         if (newMode == null) {
635             return;
636         }
637
638         this.madokaSettings.setOperationMode(newMode);
639
640         updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_OPERATION_MODE, new StringType(newMode.name()));
641
642         // For HomeKit channel, we need to map it to HomeKit supported strings
643         OnOffType ooStatus = madokaSettings.getOnOffState();
644
645         if (ooStatus != null && ooStatus == OnOffType.ON) {
646             switch (newMode) {
647                 case COOL:
648                     updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_HOMEKIT_CURRENT_HEATING_COOLING_MODE,
649                             new StringType("Cooling"));
650                     updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_HOMEBRIDGE_MODE, new DecimalType(2));
651                     break;
652                 case HEAT:
653                     updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_HOMEKIT_CURRENT_HEATING_COOLING_MODE,
654                             new StringType("Heating"));
655                     updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_HOMEBRIDGE_MODE, new DecimalType(1));
656                     break;
657                 case AUTO:
658                     updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_HOMEKIT_CURRENT_HEATING_COOLING_MODE,
659                             new StringType("Auto"));
660                     updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_HOMEBRIDGE_MODE, new DecimalType(3));
661                 default:
662                     break;
663             }
664         }
665
666         // If this is the first channel update - then we set target = current mode
667         if (this.madokaSettings.getHomekitTargetMode() == null) {
668             String newHomekitTargetStatus = null;
669
670             // For HomeKit channel, we need to map it to HomeKit supported strings
671             switch (newMode) {
672                 case COOL:
673                     newHomekitTargetStatus = "CoolOn";
674                     break;
675                 case HEAT:
676                     newHomekitTargetStatus = "HeatOn";
677                     break;
678                 default:
679                     return;
680             }
681
682             if (ooStatus != null && ooStatus == OnOffType.ON) {
683                 this.madokaSettings.setHomekitTargetMode(newHomekitTargetStatus);
684                 updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_HOMEKIT_TARGET_HEATING_COOLING_MODE,
685                         new StringType(newHomekitTargetStatus));
686             } else if (ooStatus != null && ooStatus == OnOffType.OFF) {
687                 newHomekitTargetStatus = "Off";
688                 this.madokaSettings.setHomekitTargetMode(newHomekitTargetStatus);
689                 updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_HOMEKIT_TARGET_HEATING_COOLING_MODE,
690                         new StringType(newHomekitTargetStatus));
691             }
692
693         }
694     }
695
696     @Override
697     public void receivedResponse(GetPowerstateCommand command) {
698         if (command.isPowerState() == null) {
699             return;
700         }
701
702         OnOffType oot = command.isPowerState() ? OnOffType.ON : OnOffType.OFF;
703
704         this.madokaSettings.setOnOffState(oot);
705
706         updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_ONOFF_STATUS, oot);
707
708         if (oot.equals(OnOffType.OFF)) {
709             updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_HOMEKIT_CURRENT_HEATING_COOLING_MODE,
710                     new StringType("Off"));
711             updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_HOMEKIT_TARGET_HEATING_COOLING_MODE,
712                     new StringType("Off"));
713             updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_HOMEBRIDGE_MODE, new DecimalType(0));
714         }
715     }
716
717     @Override
718     public void receivedResponse(GetIndoorOutoorTemperatures command) {
719         QuantityType<Temperature> newIndoorTemp = command.getIndoorTemperature();
720         if (newIndoorTemp != null) {
721             updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_INDOOR_TEMPERATURE, newIndoorTemp);
722             this.madokaSettings.setIndoorTemperature(newIndoorTemp);
723         }
724
725         QuantityType<Temperature> newOutdoorTemp = command.getOutdoorTemperature();
726         if (newOutdoorTemp == null) {
727             updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_OUTDOOR_TEMPERATURE, UnDefType.UNDEF);
728         } else {
729             this.madokaSettings.setOutdoorTemperature(newOutdoorTemp);
730             updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_OUTDOOR_TEMPERATURE, newOutdoorTemp);
731         }
732     }
733
734     @Override
735     public void receivedResponse(GetEyeBrightnessCommand command) {
736         PercentType eyeBrightnessTemp = command.getEyeBrightness();
737         if (eyeBrightnessTemp != null) {
738             this.madokaSettings.setEyeBrightness(eyeBrightnessTemp);
739             updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_EYE_BRIGHTNESS, eyeBrightnessTemp);
740             logger.debug("Notified {} channel with value {}", DaikinMadokaBindingConstants.CHANNEL_ID_EYE_BRIGHTNESS,
741                     eyeBrightnessTemp);
742         }
743     }
744
745     @Override
746     public void receivedResponse(SetEyeBrightnessCommand command) {
747         updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_EYE_BRIGHTNESS, command.getEyeBrightness());
748         madokaSettings.setEyeBrightness(command.getEyeBrightness());
749     }
750
751     @Override
752     public void receivedResponse(SetPowerstateCommand command) {
753         updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_ONOFF_STATUS, command.getPowerState());
754
755         madokaSettings.setOnOffState(command.getPowerState());
756
757         if (command.getPowerState() == OnOffType.ON) {
758             // Depending on the state
759
760             OperationMode operationMode = madokaSettings.getOperationMode();
761             if (operationMode == null) {
762                 return;
763             }
764
765             switch (operationMode) {
766                 case AUTO:
767                     updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_HOMEKIT_CURRENT_HEATING_COOLING_MODE,
768                             new StringType("Auto"));
769                     updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_HOMEBRIDGE_MODE, new DecimalType(3));
770                     break;
771                 case HEAT:
772                     updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_HOMEKIT_CURRENT_HEATING_COOLING_MODE,
773                             new StringType("Heating"));
774                     updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_HOMEBRIDGE_MODE, new DecimalType(1));
775                     break;
776                 case COOL:
777                     updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_HOMEKIT_CURRENT_HEATING_COOLING_MODE,
778                             new StringType("Cooling"));
779                     updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_HOMEBRIDGE_MODE, new DecimalType(2));
780                     break;
781                 default: // Other Modes are not [yet] supported
782                     break;
783             }
784         } else {
785             updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_HOMEKIT_CURRENT_HEATING_COOLING_MODE,
786                     new StringType("Off"));
787             updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_HOMEBRIDGE_MODE, new DecimalType(0));
788         }
789     }
790
791     @Override
792     public void receivedResponse(GetOperationHoursCommand command) {
793         logger.debug("receivedResponse(GetOperationHoursCommand command)");
794
795         QuantityType<Time> indoorPowerHours = command.getIndoorPowerHours();
796         QuantityType<Time> indoorOperationHours = command.getIndoorOperationHours();
797         QuantityType<Time> indoorFanHours = command.getIndoorFanHours();
798
799         if (indoorPowerHours != null) {
800             this.madokaSettings.setIndoorPowerHours(indoorPowerHours);
801             updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_INDOOR_POWER_HOURS, indoorPowerHours);
802             logger.debug("Notified {} channel with value {}",
803                     DaikinMadokaBindingConstants.CHANNEL_ID_INDOOR_POWER_HOURS, indoorPowerHours);
804         }
805
806         if (indoorOperationHours != null) {
807             this.madokaSettings.setIndoorOperationHours(indoorOperationHours);
808             updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_INDOOR_OPERATION_HOURS, indoorOperationHours);
809             logger.debug("Notified {} channel with value {}",
810                     DaikinMadokaBindingConstants.CHANNEL_ID_INDOOR_OPERATION_HOURS, indoorOperationHours);
811         }
812
813         if (indoorFanHours != null) {
814             this.madokaSettings.setIndoorFanHours(indoorFanHours);
815             updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_INDOOR_FAN_HOURS, indoorFanHours);
816             logger.debug("Notified {} channel with value {}", DaikinMadokaBindingConstants.CHANNEL_ID_INDOOR_FAN_HOURS,
817                     indoorFanHours);
818         }
819     }
820
821     @Override
822     public void receivedResponse(SetSetpointCommand command) {
823         // The update depends on the mode - so if not set - skip
824         OperationMode operationMode = this.madokaSettings.getOperationMode();
825         if (operationMode == null) {
826             return;
827         }
828
829         switch (operationMode) {
830             case HEAT:
831                 this.madokaSettings.setSetpoint(command.getHeatingSetpoint());
832                 break;
833             case COOL:
834                 this.madokaSettings.setSetpoint(command.getCoolingSetpoint());
835                 break;
836             case AUTO:
837                 // Here we don't really care if we are taking cooling or heating...
838                 this.madokaSettings.setSetpoint(command.getCoolingSetpoint());
839                 break;
840             default:
841                 return;
842         }
843
844         QuantityType<Temperature> dt = madokaSettings.getSetpoint();
845         if (dt != null) {
846             updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_SETPOINT, dt);
847         }
848     }
849
850     @Override
851     public void receivedResponse(GetCleanFilterIndicatorCommand command) {
852         Boolean indicatorStatus = command.getCleanFilterIndicator();
853         if (indicatorStatus != null) {
854             this.madokaSettings.setCleanFilterIndicator(indicatorStatus);
855             updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_CLEAN_FILTER_INDICATOR,
856                     indicatorStatus == true ? OnOffType.ON : OnOffType.OFF);
857         }
858     }
859
860     /**
861      * Received response to "SetOperationmodeCommand" command
862      */
863     @Override
864     public void receivedResponse(SetOperationmodeCommand command) {
865         this.madokaSettings.setOperationMode(command.getOperationMode());
866         updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_OPERATION_MODE,
867                 new StringType(command.getOperationMode().toString()));
868     }
869
870     /**
871      * Received response to "SetFanSpeed" command
872      */
873     @Override
874     public void receivedResponse(SetFanspeedCommand command) {
875         // The update depends on the mode - so if not set - skip
876         OperationMode operationMode = this.madokaSettings.getOperationMode();
877         if (operationMode == null) {
878             return;
879         }
880
881         FanSpeed fanSpeed;
882         switch (operationMode) {
883             case HEAT:
884                 fanSpeed = command.getHeatingFanSpeed();
885                 this.madokaSettings.setFanspeed(fanSpeed);
886                 break;
887             case COOL:
888                 fanSpeed = command.getCoolingFanSpeed();
889                 this.madokaSettings.setFanspeed(fanSpeed);
890                 break;
891             case AUTO:
892                 fanSpeed = command.getCoolingFanSpeed(); // Arbitrary cooling or heating... They are the same!
893                 this.madokaSettings.setFanspeed(fanSpeed);
894                 break;
895             default:
896                 return;
897         }
898
899         updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_FAN_SPEED, new DecimalType(fanSpeed.value()));
900     }
901
902     private void updateStateIfLinked(String channelId, State state) {
903         ChannelUID channelUID = new ChannelUID(getThing().getUID(), channelId);
904         if (isLinked(channelUID)) {
905             updateState(channelUID, state);
906         }
907     }
908 }