]> git.basschouten.com Git - openhab-addons.git/blob
d1a2ba510010c997e0a8c7f61a04e3d8186f3b65
[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.Random;
16 import java.util.concurrent.ExecutionException;
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 import java.util.concurrent.TimeoutException;
23
24 import javax.measure.quantity.Temperature;
25 import javax.measure.quantity.Time;
26
27 import org.eclipse.jdt.annotation.NonNull;
28 import org.eclipse.jdt.annotation.NonNullByDefault;
29 import org.eclipse.jdt.annotation.Nullable;
30 import org.openhab.binding.bluetooth.BluetoothCharacteristic;
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, byte[] msgBytes) {
354         if (logger.isDebugEnabled()) {
355             logger.debug("[{}] onCharacteristicUpdate({})", super.thing.getUID().getId(),
356                     HexUtils.bytesToHex(msgBytes));
357         }
358         super.onCharacteristicUpdate(characteristic, msgBytes);
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 have a 0-byte length
371         if (msgBytes.length == 0) {
372             return;
373         }
374
375         this.uartProcessor.chunkReceived(msgBytes);
376     }
377
378     private void submitCommand(BRC1HCommand command) {
379         Executor executor = commandExecutor;
380
381         if (executor != null) {
382             executor.execute(() -> processCommand(command));
383         }
384     }
385
386     private void processCommand(BRC1HCommand command) {
387         logger.debug("[{}] ProcessCommand {}", super.thing.getUID().getId(), command.getClass().getSimpleName());
388
389         try {
390             currentCommand = command;
391             uartProcessor.abandon();
392
393             if (device == null || device.getConnectionState() != ConnectionState.CONNECTED) {
394                 logger.debug("Unable to send command {} to device {}: not connected",
395                         command.getClass().getSimpleName(), address);
396                 command.setState(BRC1HCommand.State.FAILED);
397                 return;
398             }
399
400             if (!device.isServicesDiscovered()) {
401                 logger.debug("Unable to send command {} to device {}: services not resolved",
402                         command.getClass().getSimpleName(), device.getAddress());
403                 command.setState(BRC1HCommand.State.FAILED);
404                 return;
405             }
406
407             BluetoothCharacteristic charWrite = device
408                     .getCharacteristic(DaikinMadokaBindingConstants.CHAR_WRITE_WITHOUT_RESPONSE_UUID);
409             if (charWrite == null) {
410                 logger.warn("Unable to execute {}. Characteristic '{}' could not be found.",
411                         command.getClass().getSimpleName(),
412                         DaikinMadokaBindingConstants.CHAR_WRITE_WITHOUT_RESPONSE_UUID);
413                 command.setState(BRC1HCommand.State.FAILED);
414                 return;
415             }
416
417             BluetoothCharacteristic charNotif = this.device
418                     .getCharacteristic(DaikinMadokaBindingConstants.CHAR_NOTIF_UUID);
419
420             if (charNotif != null) {
421                 device.enableNotifications(charNotif);
422             }
423
424             // Commands can be composed of multiple chunks
425             for (byte[] chunk : command.getRequest()) {
426                 command.setState(BRC1HCommand.State.ENQUEUED);
427                 for (int i = 0; i < DaikinMadokaBindingConstants.WRITE_CHARACTERISTIC_MAX_RETRIES; i++) {
428                     try {
429                         device.writeCharacteristic(charWrite, chunk).get(100, TimeUnit.MILLISECONDS);
430                     } catch (InterruptedException ex) {
431                         return;
432                     } catch (ExecutionException ex) {
433                         logger.debug("Error while writing message {}: {}", command.getClass().getSimpleName(),
434                                 ex.getMessage());
435                         Thread.sleep(100);
436                         continue;
437                     } catch (TimeoutException ex) {
438                         Thread.sleep(100);
439                         continue;
440                     }
441                     command.setState(BRC1HCommand.State.SENT);
442                     break;
443                 }
444             }
445
446             if (command.getState() == BRC1HCommand.State.SENT && this.config != null) {
447                 if (!command.awaitStateChange(this.config.commandTimeout, TimeUnit.MILLISECONDS,
448                         BRC1HCommand.State.SUCCEEDED, BRC1HCommand.State.FAILED)) {
449                     logger.debug("[{}] Command {} to device {} timed out", super.thing.getUID().getId(), command,
450                             device.getAddress());
451                     command.setState(BRC1HCommand.State.FAILED);
452                 }
453             }
454         } catch (Exception e) {
455             currentCommand = null;
456             // Let the exception bubble the stack!
457             throw new RuntimeException(e);
458         }
459
460         try {
461             Thread.sleep(200);
462         } catch (InterruptedException e) {
463             Thread.currentThread().interrupt();
464         }
465     }
466
467     /**
468      * When the method is triggered, it means that all message chunks have been received, re-assembled in the right
469      * order and that the payload is ready to be processed.
470      */
471     @Override
472     public void receivedResponse(byte[] response) {
473         logger.debug("Received Response");
474         BRC1HCommand command = currentCommand;
475
476         if (command == null) {
477             if (logger.isDebugEnabled()) {
478                 logger.debug("No command present to handle response {}", HexUtils.bytesToHex(response));
479             }
480         } else {
481             try {
482                 command.handleResponse(scheduler, this, MadokaMessage.parse(response));
483             } catch (MadokaParsingException e) {
484                 logger.debug("Response message could not be parsed correctly ({}): {}. Reason: {}",
485                         command.getClass().getSimpleName(), HexUtils.bytesToHex(response), e.getMessage());
486             }
487         }
488     }
489
490     @Override
491     public void receivedResponse(GetVersionCommand command) {
492         String commCtrlVers = command.getCommunicationControllerVersion();
493         if (commCtrlVers != null) {
494             this.madokaSettings.setCommunicationControllerVersion(commCtrlVers);
495             updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_COMMUNICATION_CONTROLLER_VERSION,
496                     new StringType(commCtrlVers));
497         }
498
499         String remoteCtrlVers = command.getRemoteControllerVersion();
500         if (remoteCtrlVers != null) {
501             this.madokaSettings.setRemoteControllerVersion(remoteCtrlVers);
502             updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_REMOTE_CONTROLLER_VERSION,
503                     new StringType(remoteCtrlVers));
504         }
505     }
506
507     @Override
508     public void receivedResponse(GetFanspeedCommand command) {
509         if (command.getCoolingFanSpeed() == null || command.getHeatingFanSpeed() == null) {
510             return;
511         }
512
513         // We need the current operation mode to determine which Fan Speed we use (cooling or heating)
514         OperationMode operationMode = this.madokaSettings.getOperationMode();
515         if (operationMode == null) {
516             return;
517         }
518
519         FanSpeed fs;
520
521         switch (operationMode) {
522             case AUTO:
523                 logger.debug("In AutoMode, CoolingFanSpeed = {}, HeatingFanSpeed = {}", command.getCoolingFanSpeed(),
524                         command.getHeatingFanSpeed());
525                 fs = command.getHeatingFanSpeed();
526                 break;
527             case HEAT:
528                 fs = command.getHeatingFanSpeed();
529                 break;
530             case COOL:
531                 fs = command.getCoolingFanSpeed();
532                 break;
533             default:
534                 return;
535         }
536
537         if (fs == null) {
538             return;
539         }
540
541         // No need to re-set if it is the same value
542         if (fs.equals(this.madokaSettings.getFanspeed())) {
543             return;
544         }
545
546         this.madokaSettings.setFanspeed(fs);
547         updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_FAN_SPEED, new DecimalType(fs.value()));
548     }
549
550     @Override
551     public void receivedResponse(GetSetpointCommand command) {
552         if (command.getCoolingSetpoint() == null || command.getHeatingSetpoint() == null) {
553             return;
554         }
555
556         // We need the current operation mode to determine which Fan Speed we use (cooling or heating)
557         OperationMode operationMode = this.madokaSettings.getOperationMode();
558         if (operationMode == null) {
559             return;
560         }
561
562         QuantityType<Temperature> sp;
563
564         switch (operationMode) {
565             case AUTO:
566                 logger.debug("In AutoMode, CoolingSetpoint = {}, HeatingSetpoint = {}", command.getCoolingSetpoint(),
567                         command.getHeatingSetpoint());
568                 sp = command.getHeatingSetpoint();
569                 break;
570             case HEAT:
571                 sp = command.getHeatingSetpoint();
572                 break;
573             case COOL:
574                 sp = command.getCoolingSetpoint();
575                 break;
576             default:
577                 return;
578         }
579
580         if (sp == null) {
581             return;
582         }
583
584         // No need to re-set if it is the same value
585         if (sp.equals(this.madokaSettings.getSetpoint())) {
586             return;
587         }
588
589         this.madokaSettings.setSetpoint(sp);
590
591         QuantityType<Temperature> dt = this.madokaSettings.getSetpoint();
592         if (dt != null) {
593             updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_SETPOINT, dt);
594         }
595     }
596
597     @Override
598     public void receivedResponse(GetOperationmodeCommand command) {
599         if (command.getOperationMode() == null) {
600             logger.debug("OperationMode is null.");
601             return;
602         }
603
604         OperationMode newMode = command.getOperationMode();
605
606         if (newMode == null) {
607             return;
608         }
609
610         this.madokaSettings.setOperationMode(newMode);
611
612         updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_OPERATION_MODE, new StringType(newMode.name()));
613
614         // For HomeKit channel, we need to map it to HomeKit supported strings
615         OnOffType ooStatus = madokaSettings.getOnOffState();
616
617         if (ooStatus != null && ooStatus == OnOffType.ON) {
618             switch (newMode) {
619                 case COOL:
620                     updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_HOMEKIT_CURRENT_HEATING_COOLING_MODE,
621                             new StringType("Cooling"));
622                     updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_HOMEBRIDGE_MODE, new DecimalType(2));
623                     break;
624                 case HEAT:
625                     updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_HOMEKIT_CURRENT_HEATING_COOLING_MODE,
626                             new StringType("Heating"));
627                     updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_HOMEBRIDGE_MODE, new DecimalType(1));
628                     break;
629                 case AUTO:
630                     updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_HOMEKIT_CURRENT_HEATING_COOLING_MODE,
631                             new StringType("Auto"));
632                     updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_HOMEBRIDGE_MODE, new DecimalType(3));
633                 default:
634                     break;
635             }
636         }
637
638         // If this is the first channel update - then we set target = current mode
639         if (this.madokaSettings.getHomekitTargetMode() == null) {
640             String newHomekitTargetStatus = null;
641
642             // For HomeKit channel, we need to map it to HomeKit supported strings
643             switch (newMode) {
644                 case COOL:
645                     newHomekitTargetStatus = "CoolOn";
646                     break;
647                 case HEAT:
648                     newHomekitTargetStatus = "HeatOn";
649                     break;
650                 default:
651                     return;
652             }
653
654             if (ooStatus != null && ooStatus == OnOffType.ON) {
655                 this.madokaSettings.setHomekitTargetMode(newHomekitTargetStatus);
656                 updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_HOMEKIT_TARGET_HEATING_COOLING_MODE,
657                         new StringType(newHomekitTargetStatus));
658             } else if (ooStatus != null && ooStatus == OnOffType.OFF) {
659                 newHomekitTargetStatus = "Off";
660                 this.madokaSettings.setHomekitTargetMode(newHomekitTargetStatus);
661                 updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_HOMEKIT_TARGET_HEATING_COOLING_MODE,
662                         new StringType(newHomekitTargetStatus));
663             }
664
665         }
666     }
667
668     @Override
669     public void receivedResponse(GetPowerstateCommand command) {
670         if (command.isPowerState() == null) {
671             return;
672         }
673
674         OnOffType oot = command.isPowerState() ? OnOffType.ON : OnOffType.OFF;
675
676         this.madokaSettings.setOnOffState(oot);
677
678         updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_ONOFF_STATUS, oot);
679
680         if (oot.equals(OnOffType.OFF)) {
681             updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_HOMEKIT_CURRENT_HEATING_COOLING_MODE,
682                     new StringType("Off"));
683             updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_HOMEKIT_TARGET_HEATING_COOLING_MODE,
684                     new StringType("Off"));
685             updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_HOMEBRIDGE_MODE, new DecimalType(0));
686         }
687     }
688
689     @Override
690     public void receivedResponse(GetIndoorOutoorTemperatures command) {
691         QuantityType<Temperature> newIndoorTemp = command.getIndoorTemperature();
692         if (newIndoorTemp != null) {
693             updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_INDOOR_TEMPERATURE, newIndoorTemp);
694             this.madokaSettings.setIndoorTemperature(newIndoorTemp);
695         }
696
697         QuantityType<Temperature> newOutdoorTemp = command.getOutdoorTemperature();
698         if (newOutdoorTemp == null) {
699             updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_OUTDOOR_TEMPERATURE, UnDefType.UNDEF);
700         } else {
701             this.madokaSettings.setOutdoorTemperature(newOutdoorTemp);
702             updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_OUTDOOR_TEMPERATURE, newOutdoorTemp);
703         }
704     }
705
706     @Override
707     public void receivedResponse(GetEyeBrightnessCommand command) {
708         PercentType eyeBrightnessTemp = command.getEyeBrightness();
709         if (eyeBrightnessTemp != null) {
710             this.madokaSettings.setEyeBrightness(eyeBrightnessTemp);
711             updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_EYE_BRIGHTNESS, eyeBrightnessTemp);
712             logger.debug("Notified {} channel with value {}", DaikinMadokaBindingConstants.CHANNEL_ID_EYE_BRIGHTNESS,
713                     eyeBrightnessTemp);
714         }
715     }
716
717     @Override
718     public void receivedResponse(SetEyeBrightnessCommand command) {
719         updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_EYE_BRIGHTNESS, command.getEyeBrightness());
720         madokaSettings.setEyeBrightness(command.getEyeBrightness());
721     }
722
723     @Override
724     public void receivedResponse(SetPowerstateCommand command) {
725         updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_ONOFF_STATUS, command.getPowerState());
726
727         madokaSettings.setOnOffState(command.getPowerState());
728
729         if (command.getPowerState() == OnOffType.ON) {
730             // Depending on the state
731
732             OperationMode operationMode = madokaSettings.getOperationMode();
733             if (operationMode == null) {
734                 return;
735             }
736
737             switch (operationMode) {
738                 case AUTO:
739                     updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_HOMEKIT_CURRENT_HEATING_COOLING_MODE,
740                             new StringType("Auto"));
741                     updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_HOMEBRIDGE_MODE, new DecimalType(3));
742                     break;
743                 case HEAT:
744                     updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_HOMEKIT_CURRENT_HEATING_COOLING_MODE,
745                             new StringType("Heating"));
746                     updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_HOMEBRIDGE_MODE, new DecimalType(1));
747                     break;
748                 case COOL:
749                     updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_HOMEKIT_CURRENT_HEATING_COOLING_MODE,
750                             new StringType("Cooling"));
751                     updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_HOMEBRIDGE_MODE, new DecimalType(2));
752                     break;
753                 default: // Other Modes are not [yet] supported
754                     break;
755             }
756         } else {
757             updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_HOMEKIT_CURRENT_HEATING_COOLING_MODE,
758                     new StringType("Off"));
759             updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_HOMEBRIDGE_MODE, new DecimalType(0));
760         }
761     }
762
763     @Override
764     public void receivedResponse(GetOperationHoursCommand command) {
765         logger.debug("receivedResponse(GetOperationHoursCommand command)");
766
767         QuantityType<Time> indoorPowerHours = command.getIndoorPowerHours();
768         QuantityType<Time> indoorOperationHours = command.getIndoorOperationHours();
769         QuantityType<Time> indoorFanHours = command.getIndoorFanHours();
770
771         if (indoorPowerHours != null) {
772             this.madokaSettings.setIndoorPowerHours(indoorPowerHours);
773             updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_INDOOR_POWER_HOURS, indoorPowerHours);
774             logger.debug("Notified {} channel with value {}",
775                     DaikinMadokaBindingConstants.CHANNEL_ID_INDOOR_POWER_HOURS, indoorPowerHours);
776         }
777
778         if (indoorOperationHours != null) {
779             this.madokaSettings.setIndoorOperationHours(indoorOperationHours);
780             updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_INDOOR_OPERATION_HOURS, indoorOperationHours);
781             logger.debug("Notified {} channel with value {}",
782                     DaikinMadokaBindingConstants.CHANNEL_ID_INDOOR_OPERATION_HOURS, indoorOperationHours);
783         }
784
785         if (indoorFanHours != null) {
786             this.madokaSettings.setIndoorFanHours(indoorFanHours);
787             updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_INDOOR_FAN_HOURS, indoorFanHours);
788             logger.debug("Notified {} channel with value {}", DaikinMadokaBindingConstants.CHANNEL_ID_INDOOR_FAN_HOURS,
789                     indoorFanHours);
790         }
791     }
792
793     @Override
794     public void receivedResponse(SetSetpointCommand command) {
795         // The update depends on the mode - so if not set - skip
796         OperationMode operationMode = this.madokaSettings.getOperationMode();
797         if (operationMode == null) {
798             return;
799         }
800
801         switch (operationMode) {
802             case HEAT:
803                 this.madokaSettings.setSetpoint(command.getHeatingSetpoint());
804                 break;
805             case COOL:
806                 this.madokaSettings.setSetpoint(command.getCoolingSetpoint());
807                 break;
808             case AUTO:
809                 // Here we don't really care if we are taking cooling or heating...
810                 this.madokaSettings.setSetpoint(command.getCoolingSetpoint());
811                 break;
812             default:
813                 return;
814         }
815
816         QuantityType<Temperature> dt = madokaSettings.getSetpoint();
817         if (dt != null) {
818             updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_SETPOINT, dt);
819         }
820     }
821
822     @Override
823     public void receivedResponse(GetCleanFilterIndicatorCommand command) {
824         Boolean indicatorStatus = command.getCleanFilterIndicator();
825         if (indicatorStatus != null) {
826             this.madokaSettings.setCleanFilterIndicator(indicatorStatus);
827             updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_CLEAN_FILTER_INDICATOR,
828                     indicatorStatus == true ? OnOffType.ON : OnOffType.OFF);
829         }
830     }
831
832     /**
833      * Received response to "SetOperationmodeCommand" command
834      */
835     @Override
836     public void receivedResponse(SetOperationmodeCommand command) {
837         this.madokaSettings.setOperationMode(command.getOperationMode());
838         updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_OPERATION_MODE,
839                 new StringType(command.getOperationMode().toString()));
840     }
841
842     /**
843      * Received response to "SetFanSpeed" command
844      */
845     @Override
846     public void receivedResponse(SetFanspeedCommand command) {
847         // The update depends on the mode - so if not set - skip
848         OperationMode operationMode = this.madokaSettings.getOperationMode();
849         if (operationMode == null) {
850             return;
851         }
852
853         FanSpeed fanSpeed;
854         switch (operationMode) {
855             case HEAT:
856                 fanSpeed = command.getHeatingFanSpeed();
857                 this.madokaSettings.setFanspeed(fanSpeed);
858                 break;
859             case COOL:
860                 fanSpeed = command.getCoolingFanSpeed();
861                 this.madokaSettings.setFanspeed(fanSpeed);
862                 break;
863             case AUTO:
864                 fanSpeed = command.getCoolingFanSpeed(); // Arbitrary cooling or heating... They are the same!
865                 this.madokaSettings.setFanspeed(fanSpeed);
866                 break;
867             default:
868                 return;
869         }
870
871         updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_FAN_SPEED, new DecimalType(fanSpeed.value()));
872     }
873
874     private void updateStateIfLinked(String channelId, State state) {
875         ChannelUID channelUID = new ChannelUID(getThing().getUID(), channelId);
876         if (isLinked(channelUID)) {
877             updateState(channelUID, state);
878         }
879     }
880 }