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