From cb5d659c9ef8558a994645cf31ab1891c30b9810 Mon Sep 17 00:00:00 2001 From: Benjamin Lafois Date: Wed, 6 Jan 2021 21:50:55 +0100 Subject: [PATCH] [Daikinmadoka] New channels and fixes (#9368) * added new channels and extra fixes Signed-off-by: Benjamin Lafois * wip Signed-off-by: Benjamin Lafois * added multiple channels Signed-off-by: Benjamin Lafois * fixes after PR comments Signed-off-by: Benjamin Lafois * added support for AUTO fan mode Signed-off-by: Benjamin Lafois * Fixes units Signed-off-by: Benjamin Lafois * Fix PR Signed-off-by: Benjamin Lafois * PR fixes Signed-off-by: Benjamin Lafois * PR fixes Signed-off-by: Benjamin Lafois * Fixed copyright 2020->2021 Signed-off-by: Benjamin Lafois --- .../README.md | 8 +- .../DaikinMadokaBindingConstants.java | 9 + .../handler/DaikinMadokaHandler.java | 170 ++++++++++++++++-- .../internal/BRC1HUartProcessor.java | 38 ++-- .../internal/model/MadokaMessage.java | 38 +++- .../internal/model/MadokaProperties.java | 7 +- .../internal/model/MadokaSettings.java | 71 ++++++-- .../internal/model/MadokaValue.java | 20 ++- .../internal/model/commands/BRC1HCommand.java | 2 +- .../DisableCleanFilterIndicatorCommand.java | 51 ++++++ .../commands/EnterPrivilegedModeCommand.java | 55 ++++++ .../GetCleanFilterIndicatorCommand.java | 70 ++++++++ .../commands/GetEyeBrightnessCommand.java | 78 ++++++++ .../model/commands/GetFanspeedCommand.java | 2 +- .../commands/GetIndoorOutoorTemperatures.java | 19 +- .../commands/GetOperationHoursCommand.java | 115 ++++++++++++ .../commands/GetOperationmodeCommand.java | 2 +- .../model/commands/GetPowerstateCommand.java | 2 +- .../model/commands/GetSetpointCommand.java | 19 +- .../model/commands/GetVersionCommand.java | 2 +- .../ResetCleanFilterTimerCommand.java | 51 ++++++ .../model/commands/ResponseListener.java | 8 + .../commands/SetEyeBrightnessCommand.java | 80 +++++++++ .../model/commands/SetFanspeedCommand.java | 2 +- .../commands/SetOperationmodeCommand.java | 2 +- .../model/commands/SetPowerstateCommand.java | 2 +- .../model/commands/SetSetpointCommand.java | 16 +- .../resources/OH-INF/thing/daikinmadoka.xml | 34 ++++ .../internal/MadokaMessageTest.java | 38 +++- .../internal/UartProcessorTest.java | 132 ++++++++++++++ 30 files changed, 1060 insertions(+), 83 deletions(-) create mode 100644 bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/model/commands/DisableCleanFilterIndicatorCommand.java create mode 100644 bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/model/commands/EnterPrivilegedModeCommand.java create mode 100644 bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/model/commands/GetCleanFilterIndicatorCommand.java create mode 100644 bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/model/commands/GetEyeBrightnessCommand.java create mode 100644 bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/model/commands/GetOperationHoursCommand.java create mode 100644 bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/model/commands/ResetCleanFilterTimerCommand.java create mode 100644 bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/internal/model/commands/SetEyeBrightnessCommand.java create mode 100644 bundles/org.openhab.binding.bluetooth.daikinmadoka/src/test/java/org/openhab/binding/bluetooth/daikinmadoka/internal/UartProcessorTest.java diff --git a/bundles/org.openhab.binding.bluetooth.daikinmadoka/README.md b/bundles/org.openhab.binding.bluetooth.daikinmadoka/README.md index 29efb6f8c..1f2e2461b 100644 --- a/bundles/org.openhab.binding.bluetooth.daikinmadoka/README.md +++ b/bundles/org.openhab.binding.bluetooth.daikinmadoka/README.md @@ -49,10 +49,16 @@ _Note that it is planned to generate some part of this based on the XML files wi | commCtrlVersion | String | R | Communication Controller Firmware Version | remoteCtrlVersion | String | R | Remote Controller Firmware Version | operationMode | String | R/W | The operation mode of the AC unit. Currently supported values: HEAT, COOL. -| fanSpeed | Number | R/W | This is a "virtual channel" : its value is calculated depending on current operation mode. It is the channel to be used to change the fan speed, whatever the current mode is. Fan speed are from 1 to 5. On BRC1H, the device supports 3 speeds: LOW (1), MEDIUM (2-4), MAX (5). +| fanSpeed | Number | R/W | This is a "virtual channel" : its value is calculated depending on current operation mode. It is the channel to be used to change the fan speed, whatever the current mode is. Fan speed are from 1 to 5. On BRC1H, the device supports 3 speeds: LOW (1), MEDIUM (2-4), MAX (5). Some BRC1H also support an AUTO (0) mode - but not all of them support it (depending on internal unit). | setpoint | Number:Temperature | R/W | This is a "virtual channel" : its value is calculated depending on current operation mode. It is the channel to be used to change the setpoint, whatever the current mode is. | homekitCurrentHeatingCoolingMode | String | R | This channel is a "virtual channel" to be used with the HomeKit add-on to implement Thermostat thing. Values supported are the HomeKit addon ones: Off, CoolOn, HeatOn, Auto. | homekitTargetHeatingCoolingMode | String | R/W | This channel is a "virtual channel" to be used with the HomeKit add-on to implement Thermostat thing. Values supported are the HomeKit addon ones: Off, CoolOn, HeatOn, Auto. +| homebridgeMode | String | R/W | This channel is a "virtual channel" to be used with external HomeBridge. Values are: Off, Heating, Cooling, Auto. +| eyeBrightness | Dimmer | R/W | This channel allows to manipulate the Blue "Eye" indicator Brightness. Values are between 0 and 100. +| indoorPowerHours | Number:Time | R | This channel indicates the number of hours the indoor unit has been powered (operating or not). +| indoorOperationHours | Number:Time | R | This channel indicates the number of hours the indoor unit has been operating. +| indoorFanHours | Number:Time | R | This channel indicates the number of hours the fan has been blowing. +| cleanFilterIndicator | Switch | R/W | This channel indicates if the filter needs cleaning. The indicator can be reset by writing "OFF" to the channel. ## Full Example diff --git a/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/DaikinMadokaBindingConstants.java b/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/DaikinMadokaBindingConstants.java index bb8a863a9..4fc9b180b 100644 --- a/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/DaikinMadokaBindingConstants.java +++ b/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/DaikinMadokaBindingConstants.java @@ -30,6 +30,8 @@ public class DaikinMadokaBindingConstants { private DaikinMadokaBindingConstants() { } + public static final int WRITE_CHARACTERISTIC_MAX_RETRIES = 3; + public static final ThingTypeUID THING_TYPE_BRC1H = new ThingTypeUID(BluetoothBindingConstants.BINDING_ID, "brc1h"); public static final String CHANNEL_ID_ONOFF_STATUS = "onOffStatus"; @@ -45,6 +47,13 @@ public class DaikinMadokaBindingConstants { public static final String CHANNEL_ID_HOMEKIT_TARGET_HEATING_COOLING_MODE = "homekitTargetHeatingCoolingMode"; public static final String CHANNEL_ID_HOMEBRIDGE_MODE = "homebridgeMode"; + public static final String CHANNEL_ID_EYE_BRIGHTNESS = "eyeBrightness"; + public static final String CHANNEL_ID_INDOOR_OPERATION_HOURS = "indoorOperationHours"; + public static final String CHANNEL_ID_INDOOR_POWER_HOURS = "indoorPowerHours"; + public static final String CHANNEL_ID_INDOOR_FAN_HOURS = "indoorFanHours"; + + public static final String CHANNEL_ID_CLEAN_FILTER_INDICATOR = "cleanFilterIndicator"; + /** * BLUETOOTH UUID (service + chars) */ diff --git a/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/handler/DaikinMadokaHandler.java b/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/handler/DaikinMadokaHandler.java index 506f117db..e8169113b 100644 --- a/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/handler/DaikinMadokaHandler.java +++ b/bundles/org.openhab.binding.bluetooth.daikinmadoka/src/main/java/org/openhab/binding/bluetooth/daikinmadoka/handler/DaikinMadokaHandler.java @@ -13,12 +13,16 @@ package org.openhab.binding.bluetooth.daikinmadoka.handler; import java.util.Arrays; +import java.util.Random; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; +import javax.measure.quantity.Temperature; +import javax.measure.quantity.Time; + import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -35,13 +39,20 @@ import org.openhab.binding.bluetooth.daikinmadoka.internal.model.MadokaPropertie import org.openhab.binding.bluetooth.daikinmadoka.internal.model.MadokaProperties.OperationMode; import org.openhab.binding.bluetooth.daikinmadoka.internal.model.MadokaSettings; import org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands.BRC1HCommand; +import org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands.DisableCleanFilterIndicatorCommand; +import org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands.EnterPrivilegedModeCommand; +import org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands.GetCleanFilterIndicatorCommand; +import org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands.GetEyeBrightnessCommand; import org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands.GetFanspeedCommand; import org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands.GetIndoorOutoorTemperatures; +import org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands.GetOperationHoursCommand; import org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands.GetOperationmodeCommand; import org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands.GetPowerstateCommand; import org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands.GetSetpointCommand; import org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands.GetVersionCommand; +import org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands.ResetCleanFilterTimerCommand; import org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands.ResponseListener; +import org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands.SetEyeBrightnessCommand; import org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands.SetFanspeedCommand; import org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands.SetOperationmodeCommand; import org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands.SetPowerstateCommand; @@ -49,6 +60,7 @@ import org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands.SetSet import org.openhab.core.common.NamedThreadFactory; import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.PercentType; import org.openhab.core.library.types.QuantityType; import org.openhab.core.library.types.StringType; import org.openhab.core.thing.ChannelUID; @@ -121,7 +133,31 @@ public class DaikinMadokaHandler extends ConnectedBluetoothHandler implements Re submitCommand(new GetPowerstateCommand()); // always keep the "GetPowerState" aftern the "GetOperationMode" submitCommand(new GetSetpointCommand()); submitCommand(new GetFanspeedCommand()); - }, 10, c.refreshInterval, TimeUnit.SECONDS); + submitCommand(new GetCleanFilterIndicatorCommand()); + + try { + // As it is a complex operation - it has been extracted to a method. + retrieveOperationHours(); + } catch (InterruptedException e) { + // The thread wants to exit! + return; + } + + submitCommand(new GetEyeBrightnessCommand()); + }, new Random().nextInt(30), c.refreshInterval, TimeUnit.SECONDS); // We introduce a random start time, it + // avoids when having multiple devices to + // have the commands sent simultaneously. + } + + private void retrieveOperationHours() throws InterruptedException { + // This one is special - and MUST be ran twice, after being in priv mode + // run it once an hour is sufficient... TODO + submitCommand(new EnterPrivilegedModeCommand()); + submitCommand(new GetOperationHoursCommand()); + // a 1second+ delay is necessary + Thread.sleep(1500); + + submitCommand(new GetOperationHoursCommand()); } @Override @@ -179,15 +215,29 @@ public class DaikinMadokaHandler extends ConnectedBluetoothHandler implements Re } switch (channelUID.getId()) { + case DaikinMadokaBindingConstants.CHANNEL_ID_CLEAN_FILTER_INDICATOR: + OnOffType cleanFilterOrder = (OnOffType) command; + if (cleanFilterOrder == OnOffType.OFF) { + resetCleanFilterIndicator(); + } + break; case DaikinMadokaBindingConstants.CHANNEL_ID_SETPOINT: try { - QuantityType setpoint = (QuantityType) command; - DecimalType dt = new DecimalType(setpoint.intValue()); - submitCommand(new SetSetpointCommand(dt, dt)); + QuantityType setpoint = (QuantityType) command; + submitCommand(new SetSetpointCommand(setpoint, setpoint)); } catch (Exception e) { logger.warn("Data received is not a valid temperature.", e); } break; + case DaikinMadokaBindingConstants.CHANNEL_ID_EYE_BRIGHTNESS: + try { + logger.debug("Set eye brightness with value {}, {}", command.getClass().getName(), command); + PercentType p = (PercentType) command; + submitCommand(new SetEyeBrightnessCommand(p)); + } catch (Exception e) { + logger.warn("Data received is not a valid Eye Brightness status", e); + } + break; case DaikinMadokaBindingConstants.CHANNEL_ID_ONOFF_STATUS: try { OnOffType oot = (OnOffType) command; @@ -290,8 +340,21 @@ public class DaikinMadokaHandler extends ConnectedBluetoothHandler implements Re } } + /** + * 2 actions need to be done: disable the notification AND reset the filter timer + */ + private void resetCleanFilterIndicator() { + logger.debug("[{}] resetCleanFilterIndicator()", super.thing.getUID().getId()); + submitCommand(new DisableCleanFilterIndicatorCommand()); + submitCommand(new ResetCleanFilterTimerCommand()); + } + @Override public void onCharacteristicUpdate(BluetoothCharacteristic characteristic) { + if (logger.isDebugEnabled()) { + logger.debug("[{}] onCharacteristicUpdate({})", super.thing.getUID().getId(), + HexUtils.bytesToHex(characteristic.getByteValue())); + } super.onCharacteristicUpdate(characteristic); // Check that arguments are valid. @@ -359,14 +422,27 @@ public class DaikinMadokaHandler extends ConnectedBluetoothHandler implements Re device.enableNotifications(charNotif); } - charWrite.setValue(command.getRequest()); - command.setState(BRC1HCommand.State.ENQUEUED); - device.writeCharacteristic(charWrite); + // Commands can be composed of multiple chunks + for (byte[] chunk : command.getRequest()) { + charWrite.setValue(chunk); + command.setState(BRC1HCommand.State.ENQUEUED); + for (int i = 0; i < DaikinMadokaBindingConstants.WRITE_CHARACTERISTIC_MAX_RETRIES; i++) { + if (device.writeCharacteristic(charWrite)) { + command.setState(BRC1HCommand.State.SENT); + synchronized (command) { + command.wait(100); + } + break; + } + Thread.sleep(100); + } + } - if (this.config != null) { + if (command.getState() == BRC1HCommand.State.SENT && this.config != null) { if (!command.awaitStateChange(this.config.commandTimeout, TimeUnit.MILLISECONDS, BRC1HCommand.State.SUCCEEDED, BRC1HCommand.State.FAILED)) { - logger.debug("Command {} to device {} timed out", command, device.getAddress()); + logger.debug("[{}] Command {} to device {} timed out", super.thing.getUID().getId(), command, + device.getAddress()); command.setState(BRC1HCommand.State.FAILED); } } @@ -392,8 +468,13 @@ public class DaikinMadokaHandler extends ConnectedBluetoothHandler implements Re BRC1HCommand command = currentCommand; if (command != null) { - if (!Arrays.equals(request, command.getRequest())) { - logger.debug("Write completed for unknown command"); + // last chunk: + byte[] lastChunk = command.getRequest()[command.getRequest().length - 1]; + if (!Arrays.equals(request, lastChunk)) { + logger.debug("Write completed for a chunk, but not a complete command."); + synchronized (command) { + command.notify(); + } return; } switch (status) { @@ -506,7 +587,7 @@ public class DaikinMadokaHandler extends ConnectedBluetoothHandler implements Re return; } - DecimalType sp; + QuantityType sp; switch (operationMode) { case AUTO: @@ -535,7 +616,7 @@ public class DaikinMadokaHandler extends ConnectedBluetoothHandler implements Re this.madokaSettings.setSetpoint(sp); - DecimalType dt = this.madokaSettings.getSetpoint(); + QuantityType dt = this.madokaSettings.getSetpoint(); if (dt != null) { updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_SETPOINT, dt); } @@ -635,13 +716,13 @@ public class DaikinMadokaHandler extends ConnectedBluetoothHandler implements Re @Override public void receivedResponse(GetIndoorOutoorTemperatures command) { - DecimalType newIndoorTemp = command.getIndoorTemperature(); + QuantityType newIndoorTemp = command.getIndoorTemperature(); if (newIndoorTemp != null) { updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_INDOOR_TEMPERATURE, newIndoorTemp); this.madokaSettings.setIndoorTemperature(newIndoorTemp); } - DecimalType newOutdoorTemp = command.getOutdoorTemperature(); + QuantityType newOutdoorTemp = command.getOutdoorTemperature(); if (newOutdoorTemp == null) { updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_OUTDOOR_TEMPERATURE, UnDefType.UNDEF); } else { @@ -650,6 +731,23 @@ public class DaikinMadokaHandler extends ConnectedBluetoothHandler implements Re } } + @Override + public void receivedResponse(GetEyeBrightnessCommand command) { + PercentType eyeBrightnessTemp = command.getEyeBrightness(); + if (eyeBrightnessTemp != null) { + this.madokaSettings.setEyeBrightness(eyeBrightnessTemp); + updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_EYE_BRIGHTNESS, eyeBrightnessTemp); + logger.debug("Notified {} channel with value {}", DaikinMadokaBindingConstants.CHANNEL_ID_EYE_BRIGHTNESS, + eyeBrightnessTemp); + } + } + + @Override + public void receivedResponse(SetEyeBrightnessCommand command) { + updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_EYE_BRIGHTNESS, command.getEyeBrightness()); + madokaSettings.setEyeBrightness(command.getEyeBrightness()); + } + @Override public void receivedResponse(SetPowerstateCommand command) { updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_ONOFF_STATUS, command.getPowerState()); @@ -690,6 +788,36 @@ public class DaikinMadokaHandler extends ConnectedBluetoothHandler implements Re } } + @Override + public void receivedResponse(GetOperationHoursCommand command) { + logger.debug("receivedResponse(GetOperationHoursCommand command)"); + + QuantityType