added migrated 2.x add-ons

Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
Kai Kreuzer
2020-09-21 01:58:32 +02:00
parent bbf1a7fd29
commit 6df6783b60
11662 changed files with 1302875 additions and 11 deletions

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.bluetooth.daikinmadoka-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
<feature name="openhab-binding-bluetooth-daikinmadoka" description="DaikinMadoka Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth/${project.version}</bundle>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.bluetooth.daikinmadoka/${project.version}</bundle>
</feature>
</features>

View File

@@ -0,0 +1,54 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.bluetooth.daikinmadoka;
import java.util.UUID;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.bluetooth.BluetoothBindingConstants;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link DaikinMadokaBindingConstants} class defines common constants, which are
* used across the whole binding.
*
* @author Benjamin Lafois - Initial contribution
*/
@NonNullByDefault
public class DaikinMadokaBindingConstants {
private DaikinMadokaBindingConstants() {
}
public static final ThingTypeUID THING_TYPE_BRC1H = new ThingTypeUID(BluetoothBindingConstants.BINDING_ID, "brc1h");
public static final String CHANNEL_ID_ONOFF_STATUS = "onOffStatus";
public static final String CHANNEL_ID_INDOOR_TEMPERATURE = "indoorTemperature";
public static final String CHANNEL_ID_OUTDOOR_TEMPERATURE = "outdoorTemperature";
public static final String CHANNEL_ID_COMMUNICATION_CONTROLLER_VERSION = "commCtrlVersion";
public static final String CHANNEL_ID_REMOTE_CONTROLLER_VERSION = "remoteCtrlVersion";
public static final String CHANNEL_ID_OPERATION_MODE = "operationMode";
public static final String CHANNEL_ID_FAN_SPEED = "fanSpeed";
public static final String CHANNEL_ID_SETPOINT = "setpoint";
public static final String CHANNEL_ID_HOMEKIT_CURRENT_HEATING_COOLING_MODE = "homekitCurrentHeatingCoolingMode";
public static final String CHANNEL_ID_HOMEKIT_TARGET_HEATING_COOLING_MODE = "homekitTargetHeatingCoolingMode";
public static final String CHANNEL_ID_HOMEBRIDGE_MODE = "homebridgeMode";
/**
* BLUETOOTH UUID (service + chars)
*/
public static final UUID SERVICE_UART_UUID = UUID.fromString("2141E110-213A-11E6-B67B-9E71128CAE77");
public static final UUID CHAR_WRITE_WITHOUT_RESPONSE_UUID = UUID.fromString("2141E112-213A-11E6-B67B-9E71128CAE77");
public static final UUID CHAR_NOTIF_UUID = UUID.fromString("2141E111-213A-11E6-B67B-9E71128CAE77");
}

View File

@@ -0,0 +1,770 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.bluetooth.daikinmadoka.handler;
import java.util.Arrays;
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 org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.bluetooth.BluetoothCharacteristic;
import org.openhab.binding.bluetooth.BluetoothCompletionStatus;
import org.openhab.binding.bluetooth.BluetoothDevice.ConnectionState;
import org.openhab.binding.bluetooth.ConnectedBluetoothHandler;
import org.openhab.binding.bluetooth.daikinmadoka.DaikinMadokaBindingConstants;
import org.openhab.binding.bluetooth.daikinmadoka.internal.BRC1HUartProcessor;
import org.openhab.binding.bluetooth.daikinmadoka.internal.DaikinMadokaConfiguration;
import org.openhab.binding.bluetooth.daikinmadoka.internal.model.MadokaMessage;
import org.openhab.binding.bluetooth.daikinmadoka.internal.model.MadokaParsingException;
import org.openhab.binding.bluetooth.daikinmadoka.internal.model.MadokaProperties.FanSpeed;
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.GetFanspeedCommand;
import org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands.GetIndoorOutoorTemperatures;
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.ResponseListener;
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;
import org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands.SetSetpointCommand;
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.QuantityType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import org.openhab.core.util.HexUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link DaikinMadokaHandler} is responsible for handling commands, which are
* sent to one of the channels as well as updating channel values.
*
* @author Benjamin Lafois - Initial contribution
*/
@NonNullByDefault
public class DaikinMadokaHandler extends ConnectedBluetoothHandler implements ResponseListener {
private final Logger logger = LoggerFactory.getLogger(DaikinMadokaHandler.class);
private @Nullable DaikinMadokaConfiguration config;
private @Nullable ExecutorService commandExecutor;
private @Nullable ScheduledFuture<?> refreshJob;
// UART Processor is in charge of reassembling chunks
private BRC1HUartProcessor uartProcessor = new BRC1HUartProcessor(this);
private volatile @Nullable BRC1HCommand currentCommand = null;
private MadokaSettings madokaSettings = new MadokaSettings();
public DaikinMadokaHandler(Thing thing) {
super(thing);
}
@Override
public void initialize() {
super.initialize();
logger.debug("[{}] Start initializing!", super.thing.getUID().getId());
// Load Configuration
config = getConfigAs(DaikinMadokaConfiguration.class);
DaikinMadokaConfiguration c = config;
logger.debug("[{}] Parameter value [refreshInterval]: {}", super.thing.getUID().getId(), c.refreshInterval);
logger.debug("[{}] Parameter value [commandTimeout]: {}", super.thing.getUID().getId(), c.commandTimeout);
if (getBridge() == null) {
logger.debug("[{}] Bridge is null. Exiting.", super.thing.getUID().getId());
return;
}
this.commandExecutor = Executors
.newSingleThreadExecutor(new NamedThreadFactory(thing.getUID().getAsString(), true));
this.refreshJob = scheduler.scheduleWithFixedDelay(() -> {
// It is useless to refresh version all the time ! Just once.
if (this.madokaSettings.getCommunicationControllerVersion() == null
|| this.madokaSettings.getRemoteControllerVersion() == null) {
submitCommand(new GetVersionCommand());
}
submitCommand(new GetIndoorOutoorTemperatures());
submitCommand(new GetOperationmodeCommand());
submitCommand(new GetPowerstateCommand()); // always keep the "GetPowerState" aftern the "GetOperationMode"
submitCommand(new GetSetpointCommand());
submitCommand(new GetFanspeedCommand());
}, 10, c.refreshInterval, TimeUnit.SECONDS);
}
@Override
public void dispose() {
logger.debug("[{}] dispose()", super.thing.getUID().getId());
dispose(refreshJob);
dispose(commandExecutor);
dispose(currentCommand);
// Unsubscribe to characteristic notifications
if (this.device != null) {
BluetoothCharacteristic charNotif = this.device
.getCharacteristic(DaikinMadokaBindingConstants.CHAR_NOTIF_UUID);
if (charNotif != null) {
@NonNull
BluetoothCharacteristic c = charNotif;
this.device.disableNotifications(c);
}
}
super.dispose();
}
private static void dispose(@Nullable ExecutorService executor) {
if (executor != null) {
executor.shutdownNow();
}
}
private static void dispose(@Nullable ScheduledFuture<?> future) {
if (future != null) {
future.cancel(true);
}
}
private static void dispose(@Nullable BRC1HCommand command) {
if (command != null) {
// even if it already completed it doesn't really matter.
// on the off chance that the commandExecutor is waiting on the command, we can wake it up and cause it to
// terminate
command.setState(BRC1HCommand.State.FAILED);
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
logger.debug("[{}] Channel: {}, Command: {}", super.thing.getUID().getId(), channelUID, command);
if (command instanceof RefreshType) {
// The refresh commands are not supported in query mode.
// The binding will notify updates on channels
return;
}
switch (channelUID.getId()) {
case DaikinMadokaBindingConstants.CHANNEL_ID_SETPOINT:
try {
QuantityType<?> setpoint = (QuantityType<?>) command;
DecimalType dt = new DecimalType(setpoint.intValue());
submitCommand(new SetSetpointCommand(dt, dt));
} catch (Exception e) {
logger.warn("Data received is not a valid temperature.", e);
}
break;
case DaikinMadokaBindingConstants.CHANNEL_ID_ONOFF_STATUS:
try {
OnOffType oot = (OnOffType) command;
submitCommand(new SetPowerstateCommand(oot));
} catch (Exception e) {
logger.warn("Data received is not a valid on/off status", e);
}
break;
case DaikinMadokaBindingConstants.CHANNEL_ID_FAN_SPEED:
try {
DecimalType fanSpeed = (DecimalType) command;
FanSpeed fs = FanSpeed.valueOf(fanSpeed.intValue());
submitCommand(new SetFanspeedCommand(fs, fs));
} catch (Exception e) {
logger.warn("Data received is not a valid FanSpeed status", e);
}
break;
case DaikinMadokaBindingConstants.CHANNEL_ID_OPERATION_MODE:
try {
StringType operationMode = (StringType) command;
OperationMode m = OperationMode.valueOf(operationMode.toFullString());
submitCommand(new SetOperationmodeCommand(m));
} catch (Exception e) {
logger.warn("Data received is not a valid OPERATION MODE", e);
}
break;
case DaikinMadokaBindingConstants.CHANNEL_ID_HOMEBRIDGE_MODE:
try {
// Homebridge are discrete value different from Daikin
// 0 - Off
// 1 - Heating
// 2 - Cooling
// 3 - Auto
DecimalType homebridgeMode = (DecimalType) command;
switch (homebridgeMode.intValue()) {
case 0: // Off
submitCommand(new SetPowerstateCommand(OnOffType.OFF));
break;
case 1: // Heating
submitCommand(new SetOperationmodeCommand(OperationMode.HEAT));
if (madokaSettings.getOnOffState() == OnOffType.OFF) {
submitCommand(new SetPowerstateCommand(OnOffType.ON));
}
break;
case 2: // Cooling
submitCommand(new SetOperationmodeCommand(OperationMode.COOL));
if (madokaSettings.getOnOffState() == OnOffType.OFF) {
submitCommand(new SetPowerstateCommand(OnOffType.ON));
}
break;
case 3: // Auto
submitCommand(new SetOperationmodeCommand(OperationMode.AUTO));
if (madokaSettings.getOnOffState() == OnOffType.OFF) {
submitCommand(new SetPowerstateCommand(OnOffType.ON));
}
break;
default: // Invalid Value - in case of new FW
logger.warn("Invalid value received for channel {}. Ignoring.", channelUID);
break;
}
} catch (Exception e) {
logger.warn("Data received is not a valid HOMEBRIDGE OPERATION MODE", e);
}
break;
case DaikinMadokaBindingConstants.CHANNEL_ID_HOMEKIT_TARGET_HEATING_COOLING_MODE:
try {
StringType homekitOperationMode = (StringType) command;
switch (homekitOperationMode.toString()) {
case "Off":
submitCommand(new SetPowerstateCommand(OnOffType.OFF));
break;
case "CoolOn":
submitCommand(new SetOperationmodeCommand(OperationMode.COOL));
if (madokaSettings.getOnOffState() == OnOffType.OFF) {
submitCommand(new SetPowerstateCommand(OnOffType.ON));
}
break;
case "HeatOn":
submitCommand(new SetOperationmodeCommand(OperationMode.HEAT));
if (madokaSettings.getOnOffState() == OnOffType.OFF) {
submitCommand(new SetPowerstateCommand(OnOffType.ON));
}
break;
case "Auto":
submitCommand(new SetOperationmodeCommand(OperationMode.AUTO));
if (madokaSettings.getOnOffState() == OnOffType.OFF) {
submitCommand(new SetPowerstateCommand(OnOffType.ON));
}
break;
default:
break;
}
} catch (Exception e) {
logger.info("Error while setting mode through HomeKIt received Mode");
}
default:
break;
}
}
@Override
public void onCharacteristicUpdate(BluetoothCharacteristic characteristic) {
super.onCharacteristicUpdate(characteristic);
// Check that arguments are valid.
if (characteristic.getUuid() == null) {
return;
}
// We are only interested in the Notify Characteristic of UART service
if (!characteristic.getUuid().equals(DaikinMadokaBindingConstants.CHAR_NOTIF_UUID)) {
return;
}
// A message cannot be null or have a 0-byte length
byte[] msgBytes = characteristic.getByteValue();
if (msgBytes == null || msgBytes.length == 0) {
return;
}
this.uartProcessor.chunkReceived(msgBytes);
}
private void submitCommand(BRC1HCommand command) {
Executor executor = commandExecutor;
if (executor != null) {
executor.execute(() -> processCommand(command));
}
}
private void processCommand(BRC1HCommand command) {
logger.debug("[{}] ProcessCommand {}", super.thing.getUID().getId(), command.getClass().getSimpleName());
try {
currentCommand = command;
uartProcessor.abandon();
if (device == null || device.getConnectionState() != ConnectionState.CONNECTED) {
logger.debug("Unable to send command {} to device {}: not connected",
command.getClass().getSimpleName(), address);
command.setState(BRC1HCommand.State.FAILED);
return;
}
if (!resolved) {
logger.debug("Unable to send command {} to device {}: services not resolved",
command.getClass().getSimpleName(), device.getAddress());
command.setState(BRC1HCommand.State.FAILED);
return;
}
BluetoothCharacteristic charWrite = device
.getCharacteristic(DaikinMadokaBindingConstants.CHAR_WRITE_WITHOUT_RESPONSE_UUID);
if (charWrite == null) {
logger.warn("Unable to execute {}. Characteristic '{}' could not be found.",
command.getClass().getSimpleName(),
DaikinMadokaBindingConstants.CHAR_WRITE_WITHOUT_RESPONSE_UUID);
command.setState(BRC1HCommand.State.FAILED);
return;
}
BluetoothCharacteristic charNotif = this.device
.getCharacteristic(DaikinMadokaBindingConstants.CHAR_NOTIF_UUID);
if (charNotif != null) {
device.enableNotifications(charNotif);
}
charWrite.setValue(command.getRequest());
command.setState(BRC1HCommand.State.ENQUEUED);
device.writeCharacteristic(charWrite);
if (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());
command.setState(BRC1HCommand.State.FAILED);
}
}
} catch (Exception e) {
currentCommand = null;
// Let the exception bubble the stack!
throw new RuntimeException(e);
}
try {
Thread.sleep(200);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
@Override
public void onCharacteristicWriteComplete(BluetoothCharacteristic characteristic,
BluetoothCompletionStatus status) {
super.onCharacteristicWriteComplete(characteristic, status);
byte[] request = characteristic.getByteValue();
BRC1HCommand command = currentCommand;
if (command != null) {
if (!Arrays.equals(request, command.getRequest())) {
logger.debug("Write completed for unknown command");
return;
}
switch (status) {
case SUCCESS:
command.setState(BRC1HCommand.State.SENT);
break;
case ERROR:
command.setState(BRC1HCommand.State.FAILED);
break;
}
} else {
if (logger.isDebugEnabled()) {
logger.debug("No command found that matches request {}", HexUtils.bytesToHex(request));
}
}
}
/**
* When the method is triggered, it means that all message chunks have been received, re-assembled in the right
* order and that the payload is ready to be processed.
*/
@Override
public void receivedResponse(byte[] response) {
logger.debug("Received Response");
BRC1HCommand command = currentCommand;
if (command == null) {
if (logger.isDebugEnabled()) {
logger.debug("No command present to handle response {}", HexUtils.bytesToHex(response));
}
} else {
try {
command.handleResponse(scheduler, this, MadokaMessage.parse(response));
} catch (MadokaParsingException e) {
logger.debug("Response message could not be parsed correctly ({}): {}. Reason: {}",
command.getClass().getSimpleName(), HexUtils.bytesToHex(response), e.getMessage());
}
}
}
@Override
public void receivedResponse(GetVersionCommand command) {
String commCtrlVers = command.getCommunicationControllerVersion();
if (commCtrlVers != null) {
this.madokaSettings.setCommunicationControllerVersion(commCtrlVers);
updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_COMMUNICATION_CONTROLLER_VERSION,
new StringType(commCtrlVers));
}
String remoteCtrlVers = command.getRemoteControllerVersion();
if (remoteCtrlVers != null) {
this.madokaSettings.setRemoteControllerVersion(remoteCtrlVers);
updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_REMOTE_CONTROLLER_VERSION,
new StringType(remoteCtrlVers));
}
}
@Override
public void receivedResponse(GetFanspeedCommand command) {
if (command.getCoolingFanSpeed() == null || command.getHeatingFanSpeed() == null) {
return;
}
// We need the current operation mode to determine which Fan Speed we use (cooling or heating)
OperationMode operationMode = this.madokaSettings.getOperationMode();
if (operationMode == null) {
return;
}
FanSpeed fs;
switch (operationMode) {
case AUTO:
logger.debug("In AutoMode, CoolingFanSpeed = {}, HeatingFanSpeed = {}", command.getCoolingFanSpeed(),
command.getHeatingFanSpeed());
fs = command.getHeatingFanSpeed();
break;
case HEAT:
fs = command.getHeatingFanSpeed();
break;
case COOL:
fs = command.getCoolingFanSpeed();
break;
default:
return;
}
if (fs == null) {
return;
}
// No need to re-set if it is the same value
if (fs.equals(this.madokaSettings.getFanspeed())) {
return;
}
this.madokaSettings.setFanspeed(fs);
updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_FAN_SPEED, new DecimalType(fs.value()));
}
@Override
public void receivedResponse(GetSetpointCommand command) {
if (command.getCoolingSetpoint() == null || command.getHeatingSetpoint() == null) {
return;
}
// We need the current operation mode to determine which Fan Speed we use (cooling or heating)
OperationMode operationMode = this.madokaSettings.getOperationMode();
if (operationMode == null) {
return;
}
DecimalType sp;
switch (operationMode) {
case AUTO:
logger.debug("In AutoMode, CoolingSetpoint = {}, HeatingSetpoint = {}", command.getCoolingSetpoint(),
command.getHeatingSetpoint());
sp = command.getHeatingSetpoint();
break;
case HEAT:
sp = command.getHeatingSetpoint();
break;
case COOL:
sp = command.getCoolingSetpoint();
break;
default:
return;
}
if (sp == null) {
return;
}
// No need to re-set if it is the same value
if (sp.equals(this.madokaSettings.getSetpoint())) {
return;
}
this.madokaSettings.setSetpoint(sp);
DecimalType dt = this.madokaSettings.getSetpoint();
if (dt != null) {
updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_SETPOINT, dt);
}
}
@Override
public void receivedResponse(GetOperationmodeCommand command) {
if (command.getOperationMode() == null) {
logger.debug("OperationMode is null.");
return;
}
OperationMode newMode = command.getOperationMode();
if (newMode == null) {
return;
}
this.madokaSettings.setOperationMode(newMode);
updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_OPERATION_MODE, new StringType(newMode.name()));
// For HomeKit channel, we need to map it to HomeKit supported strings
OnOffType ooStatus = madokaSettings.getOnOffState();
if (ooStatus != null && ooStatus == OnOffType.ON) {
switch (newMode) {
case COOL:
updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_HOMEKIT_CURRENT_HEATING_COOLING_MODE,
new StringType("Cooling"));
updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_HOMEBRIDGE_MODE, new DecimalType(2));
break;
case HEAT:
updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_HOMEKIT_CURRENT_HEATING_COOLING_MODE,
new StringType("Heating"));
updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_HOMEBRIDGE_MODE, new DecimalType(1));
break;
case AUTO:
updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_HOMEKIT_CURRENT_HEATING_COOLING_MODE,
new StringType("Auto"));
updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_HOMEBRIDGE_MODE, new DecimalType(3));
default:
break;
}
}
// If this is the first channel update - then we set target = current mode
if (this.madokaSettings.getHomekitTargetMode() == null) {
String newHomekitTargetStatus = null;
// For HomeKit channel, we need to map it to HomeKit supported strings
switch (newMode) {
case COOL:
newHomekitTargetStatus = "CoolOn";
break;
case HEAT:
newHomekitTargetStatus = "HeatOn";
break;
default:
return;
}
if (ooStatus != null && ooStatus == OnOffType.ON) {
this.madokaSettings.setHomekitTargetMode(newHomekitTargetStatus);
updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_HOMEKIT_TARGET_HEATING_COOLING_MODE,
new StringType(newHomekitTargetStatus));
} else if (ooStatus != null && ooStatus == OnOffType.OFF) {
newHomekitTargetStatus = "Off";
this.madokaSettings.setHomekitTargetMode(newHomekitTargetStatus);
updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_HOMEKIT_TARGET_HEATING_COOLING_MODE,
new StringType(newHomekitTargetStatus));
}
}
}
@Override
public void receivedResponse(GetPowerstateCommand command) {
if (command.isPowerState() == null) {
return;
}
OnOffType oot = command.isPowerState() ? OnOffType.ON : OnOffType.OFF;
this.madokaSettings.setOnOffState(oot);
updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_ONOFF_STATUS, oot);
if (oot.equals(OnOffType.OFF)) {
updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_HOMEKIT_CURRENT_HEATING_COOLING_MODE,
new StringType("Off"));
updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_HOMEKIT_TARGET_HEATING_COOLING_MODE,
new StringType("Off"));
updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_HOMEBRIDGE_MODE, new DecimalType(0));
}
}
@Override
public void receivedResponse(GetIndoorOutoorTemperatures command) {
DecimalType newIndoorTemp = command.getIndoorTemperature();
if (newIndoorTemp != null) {
updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_INDOOR_TEMPERATURE, newIndoorTemp);
this.madokaSettings.setIndoorTemperature(newIndoorTemp);
}
DecimalType newOutdoorTemp = command.getOutdoorTemperature();
if (newOutdoorTemp == null) {
updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_OUTDOOR_TEMPERATURE, UnDefType.UNDEF);
} else {
this.madokaSettings.setOutdoorTemperature(newOutdoorTemp);
updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_OUTDOOR_TEMPERATURE, newOutdoorTemp);
}
}
@Override
public void receivedResponse(SetPowerstateCommand command) {
updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_ONOFF_STATUS, command.getPowerState());
madokaSettings.setOnOffState(command.getPowerState());
if (command.getPowerState() == OnOffType.ON) {
// Depending on the state
OperationMode operationMode = madokaSettings.getOperationMode();
if (operationMode == null) {
return;
}
switch (operationMode) {
case AUTO:
updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_HOMEKIT_CURRENT_HEATING_COOLING_MODE,
new StringType("Auto"));
updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_HOMEBRIDGE_MODE, new DecimalType(3));
break;
case HEAT:
updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_HOMEKIT_CURRENT_HEATING_COOLING_MODE,
new StringType("Heating"));
updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_HOMEBRIDGE_MODE, new DecimalType(1));
break;
case COOL:
updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_HOMEKIT_CURRENT_HEATING_COOLING_MODE,
new StringType("Cooling"));
updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_HOMEBRIDGE_MODE, new DecimalType(2));
break;
default: // Other Modes are not [yet] supported
break;
}
} else {
updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_HOMEKIT_CURRENT_HEATING_COOLING_MODE,
new StringType("Off"));
updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_HOMEBRIDGE_MODE, new DecimalType(0));
}
}
@Override
public void receivedResponse(SetSetpointCommand command) {
// The update depends on the mode - so if not set - skip
OperationMode operationMode = this.madokaSettings.getOperationMode();
if (operationMode == null) {
return;
}
switch (operationMode) {
case HEAT:
this.madokaSettings.setSetpoint(command.getHeatingSetpoint());
break;
case COOL:
this.madokaSettings.setSetpoint(command.getCoolingSetpoint());
break;
case AUTO:
// Here we don't really care if we are taking cooling or heating...
this.madokaSettings.setSetpoint(command.getCoolingSetpoint());
break;
default:
return;
}
DecimalType dt = madokaSettings.getSetpoint();
if (dt != null) {
updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_SETPOINT, dt);
}
}
/**
* Received response to "SetOperationmodeCommand" command
*/
@Override
public void receivedResponse(SetOperationmodeCommand command) {
this.madokaSettings.setOperationMode(command.getOperationMode());
updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_OPERATION_MODE,
new StringType(command.getOperationMode().toString()));
}
/**
* Received response to "SetFanSpeed" command
*/
@Override
public void receivedResponse(SetFanspeedCommand command) {
// The update depends on the mode - so if not set - skip
OperationMode operationMode = this.madokaSettings.getOperationMode();
if (operationMode == null) {
return;
}
FanSpeed fanSpeed;
switch (operationMode) {
case HEAT:
fanSpeed = command.getHeatingFanSpeed();
this.madokaSettings.setFanspeed(fanSpeed);
break;
case COOL:
fanSpeed = command.getCoolingFanSpeed();
this.madokaSettings.setFanspeed(fanSpeed);
break;
case AUTO:
fanSpeed = command.getCoolingFanSpeed(); // Arbitrary cooling or heating... They are the same!
this.madokaSettings.setFanspeed(fanSpeed);
break;
default:
return;
}
updateStateIfLinked(DaikinMadokaBindingConstants.CHANNEL_ID_FAN_SPEED, new DecimalType(fanSpeed.value()));
}
private void updateStateIfLinked(String channelId, State state) {
ChannelUID channelUID = new ChannelUID(getThing().getUID(), channelId);
if (isLinked(channelUID)) {
updateState(channelUID, state);
}
}
}

View File

@@ -0,0 +1,102 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.bluetooth.daikinmadoka.internal;
import java.io.ByteArrayOutputStream;
import java.util.Comparator;
import java.util.concurrent.ConcurrentSkipListSet;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands.ResponseListener;
/**
* As the protocol emutes an UART communication over BLE (characteristics write/notify), this class takes care of BLE
* transport.
*
* @author Benjamin Lafois - Initial contribution
*/
@NonNullByDefault
public class BRC1HUartProcessor {
/**
* Maximum number of bytes per message chunk, including headers
*/
public static final int MAX_CHUNK_SIZE = 20;
/**
* In the unlikely event of messages arrive in wrong order, this comparator will sort the queue
*/
private Comparator<byte[]> chunkSorter = (byte[] m1, byte[] m2) -> m1[0] - m2[0];
private ConcurrentSkipListSet<byte[]> uartMessages = new ConcurrentSkipListSet<>(chunkSorter);
private ResponseListener responseListener;
public BRC1HUartProcessor(ResponseListener responseListener) {
this.responseListener = responseListener;
}
private boolean isMessageComplete() {
int messagesInQueue = this.uartMessages.size();
if (messagesInQueue <= 0) {
return false;
}
byte[] firstMessageInQueue = uartMessages.first();
if (firstMessageInQueue.length < 2) {
return false;
}
int expectedChunks = (int) Math.ceil(firstMessageInQueue[1] / (MAX_CHUNK_SIZE - 1.0));
if (expectedChunks != messagesInQueue) {
return false;
}
// Check that we have every single ID
int expected = 0;
for (byte[] m : this.uartMessages) {
if (m.length < 2) {
return false;
}
if (m[0] != expected++) {
return false;
}
}
return true;
}
public void chunkReceived(byte[] byteValue) {
this.uartMessages.add(byteValue);
if (isMessageComplete()) {
// Beyond this point, full message received
ByteArrayOutputStream bos = new ByteArrayOutputStream();
for (byte[] msg : uartMessages) {
if (msg.length > 1) {
bos.write(msg, 1, msg.length - 1);
}
}
this.uartMessages.clear();
this.responseListener.receivedResponse(bos.toByteArray());
}
}
public void abandon() {
this.uartMessages.clear();
}
}

View File

@@ -0,0 +1,25 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.bluetooth.daikinmadoka.internal;
/**
* The {@link DaikinMadokaConfiguration} class contains fields mapping thing configuration parameters.
*
* @author Benjamin Lafois - Initial contribution
*/
public class DaikinMadokaConfiguration {
public String address;
public Integer refreshInterval;
public Integer commandTimeout;
}

View File

@@ -0,0 +1,65 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.bluetooth.daikinmadoka.internal;
import static org.openhab.binding.bluetooth.daikinmadoka.DaikinMadokaBindingConstants.THING_TYPE_BRC1H;
import java.util.Collections;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.bluetooth.daikinmadoka.handler.DaikinMadokaHandler;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link DaikinMadokaHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Benjamin Lafois - Initial contribution
*/
@NonNullByDefault
@Component(configurationPid = "binding.bluetooth.daikinmadoka", service = ThingHandlerFactory.class)
public class DaikinMadokaHandlerFactory extends BaseThingHandlerFactory {
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections.singleton(THING_TYPE_BRC1H);
private final Logger logger = LoggerFactory.getLogger(DaikinMadokaHandlerFactory.class);
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
logger.debug("Request to create handler for thing {}", thing.getThingTypeUID());
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (THING_TYPE_BRC1H.equals(thingTypeUID)) {
logger.debug("Thing is matching BRC1H");
return new DaikinMadokaHandler(thing);
}
return null;
}
}

View File

@@ -0,0 +1,153 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.bluetooth.daikinmadoka.internal.model;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands.BRC1HCommand;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This class represents a message transmitted or received from the BRC1H controller as a serial protocol
*
* @author Benjamin Lafois - Initial contribution
*/
@NonNullByDefault
public class MadokaMessage {
private static final Logger logger = LoggerFactory.getLogger(MadokaMessage.class);
private int messageId;
private final Map<Integer, MadokaValue> values;
private byte @Nullable [] rawMessage;
private MadokaMessage() {
values = new HashMap<>();
}
public static byte[] createRequest(BRC1HCommand command, MadokaValue... parameters) {
try {
ByteArrayOutputStream output = new ByteArrayOutputStream();
DataOutputStream request = new DataOutputStream(output);
// Message Length - Computed in the end
request.writeByte(0);
request.writeByte(0);
// Command ID, coded on 3 bytes
request.writeByte(0);
request.writeShort(command.getCommandId());
if (parameters.length == 0) {
request.writeByte(0);
request.writeByte(0);
} else {
for (MadokaValue mv : parameters) {
request.writeByte(mv.getId());
request.writeByte(mv.getSize());
request.write(mv.getRawValue());
}
}
// Finally, compute array size
byte[] ret = output.toByteArray();
ret[1] = (byte) (ret.length - 1);
return ret;
} catch (IOException e) {
logger.info("Error while building request", e);
throw new RuntimeException(e);
}
}
public static MadokaMessage parse(byte[] msg) throws MadokaParsingException {
// Msg format (bytes):
// <Msg Length> <msg id> <msg id> <msg id> ...
// So MINIMAL length is 4, to cover the message length + message ID
if (msg.length < 4) {
throw new MadokaParsingException("Message received is too short to be parsed.");
}
if (msg[0] != msg.length) {
throw new MadokaParsingException("Message size is not valid (different from byte[0]).");
}
MadokaMessage m = new MadokaMessage();
m.setRawMessage(msg);
m.messageId = ByteBuffer.wrap(msg, 2, 2).getShort();
MadokaValue mv = null;
// Starting here, we are not on the safe side with previous msg.length check
for (int i = 4; i < msg.length;) {
if ((i + 1) >= msg.length) {
throw new MadokaParsingException("Truncated message detected while parsing response value header");
}
mv = new MadokaValue();
mv.setId(msg[i]);
mv.setSize(Byte.toUnsignedInt(msg[i + 1]));
if ((i + 1 + mv.getSize()) >= msg.length) {
throw new MadokaParsingException("Truncated message detected while parsing response value content");
}
mv.setRawValue(Arrays.copyOfRange(msg, i + 2, i + 2 + mv.getSize()));
i += 2 + mv.getSize();
m.values.put(mv.getId(), mv);
}
return m;
}
private void setRawMessage(byte[] rawMessage) {
this.rawMessage = rawMessage;
}
public byte @Nullable [] getRawMessage() {
return this.rawMessage;
}
public int getMessageId() {
return messageId;
}
public Map<Integer, MadokaValue> getValues() {
return values;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(String.format("{ messageId: %d, values: [", this.messageId));
for (Map.Entry<Integer, MadokaValue> entry : values.entrySet()) {
sb.append(String.format(" { valueId: %d, valueSize: %d },", entry.getKey(), entry.getValue().getSize()));
}
sb.append("] }");
return sb.toString();
}
}

View File

@@ -0,0 +1,34 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.bluetooth.daikinmadoka.internal.model;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* This exception is thrown when an exception happens parsing a message from the BLE controller.
*
* @author Benjamin Lafois - Initial contribution
*
*/
@SuppressWarnings("serial")
@NonNullByDefault
public class MadokaParsingException extends Exception {
public MadokaParsingException(String message) {
super(message);
}
public MadokaParsingException(Throwable t) {
super(t);
}
}

View File

@@ -0,0 +1,80 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.bluetooth.daikinmadoka.internal.model;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* This class contains the enums for the various modes supported by the BLE thermostat
*
* @author Benjamin Lafois - Initial contribution
*
*/
@NonNullByDefault
public class MadokaProperties {
public enum FanSpeed {
MAX(5),
MEDIUM(3),
LOW(1);
private int v;
FanSpeed(int v) {
this.v = v;
}
public static FanSpeed valueOf(int v) {
if (v == 5) {
return MAX;
} else if (v >= 2 && v <= 4) {
return MEDIUM;
} else {
return LOW;
}
}
public int value() {
return v;
}
}
public enum OperationMode {
FAN(0),
DRY(1),
AUTO(2),
COOL(3),
HEAT(4),
VENTILATION(5);
private int v;
OperationMode(int v) {
this.v = v;
}
public static OperationMode valueOf(int v) {
for (OperationMode m : values()) {
if (m.v == v) {
return m;
}
}
// Should never happen
return HEAT;
}
public int value() {
return v;
}
}
}

View File

@@ -0,0 +1,127 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.bluetooth.daikinmadoka.internal.model;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.bluetooth.daikinmadoka.internal.model.MadokaProperties.FanSpeed;
import org.openhab.binding.bluetooth.daikinmadoka.internal.model.MadokaProperties.OperationMode;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
/**
* This class contains the current state of the controllerw
*
* @author Benjamin Lafois - Initial contribution
*
*/
@NonNullByDefault
public class MadokaSettings {
private @Nullable OnOffType onOffState;
private @Nullable DecimalType setpoint;
private @Nullable DecimalType indoorTemperature;
private @Nullable DecimalType outdoorTemperature;
private @Nullable FanSpeed fanspeed;
private @Nullable OperationMode operationMode;
private @Nullable String homekitCurrentMode;
private @Nullable String homekitTargetMode;
private @Nullable String communicationControllerVersion;
private @Nullable String remoteControllerVersion;
public @Nullable OnOffType getOnOffState() {
return onOffState;
}
public void setOnOffState(OnOffType onOffState) {
this.onOffState = onOffState;
}
public @Nullable DecimalType getSetpoint() {
return setpoint;
}
public void setSetpoint(DecimalType setpoint) {
this.setpoint = setpoint;
}
public @Nullable DecimalType getIndoorTemperature() {
return indoorTemperature;
}
public void setIndoorTemperature(DecimalType indoorTemperature) {
this.indoorTemperature = indoorTemperature;
}
public @Nullable DecimalType getOutdoorTemperature() {
return outdoorTemperature;
}
public void setOutdoorTemperature(DecimalType outdoorTemperature) {
this.outdoorTemperature = outdoorTemperature;
}
public @Nullable FanSpeed getFanspeed() {
return fanspeed;
}
public void setFanspeed(FanSpeed fanspeed) {
this.fanspeed = fanspeed;
}
public @Nullable OperationMode getOperationMode() {
return operationMode;
}
public void setOperationMode(OperationMode operationMode) {
this.operationMode = operationMode;
}
public @Nullable String getHomekitCurrentMode() {
return homekitCurrentMode;
}
public void setHomekitCurrentMode(String homekitCurrentMode) {
this.homekitCurrentMode = homekitCurrentMode;
}
public @Nullable String getHomekitTargetMode() {
return homekitTargetMode;
}
public void setHomekitTargetMode(String homekitTargetMode) {
this.homekitTargetMode = homekitTargetMode;
}
public @Nullable String getCommunicationControllerVersion() {
return communicationControllerVersion;
}
public void setCommunicationControllerVersion(String communicationControllerVersion) {
this.communicationControllerVersion = communicationControllerVersion;
}
public @Nullable String getRemoteControllerVersion() {
return remoteControllerVersion;
}
public void setRemoteControllerVersion(String remoteControllerVersion) {
this.remoteControllerVersion = remoteControllerVersion;
}
}

View File

@@ -0,0 +1,82 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.bluetooth.daikinmadoka.internal.model;
import java.nio.ByteBuffer;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* This class is in charge of serializing/deserializing values from a message
*
* @author Benjamin Lafois - Initial contribution
*/
@NonNullByDefault
public class MadokaValue {
private int id;
private int size;
private byte @Nullable [] rawValue;
public MadokaValue(int id, int size, byte[] rawValue) {
this.id = id;
this.size = size;
this.rawValue = rawValue;
}
public MadokaValue() {
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public int getSize() {
return size;
}
public void setSize(int size) {
this.size = size;
}
public byte @Nullable [] getRawValue() {
return rawValue;
}
public void setRawValue(byte[] rawValue) {
this.rawValue = rawValue;
}
public long getComputedValue() {
byte[] v = rawValue;
if (v != null) {
switch (size) {
case 1:
return v[0];
case 2:
return ByteBuffer.wrap(v, 0, 2).getShort();
case 4:
return ByteBuffer.wrap(v, 0, 4).getInt();
default:
// unsupported
break;
}
}
return 0;
}
}

View File

@@ -0,0 +1,113 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.bluetooth.daikinmadoka.internal.model.MadokaMessage;
import org.openhab.binding.bluetooth.daikinmadoka.internal.model.MadokaParsingException;
/**
* Abstract class for all BLE commands sent to the controller
*
* @author Benjamin Lafois - Initial contribution
*
*/
@NonNullByDefault
public abstract class BRC1HCommand {
public enum State {
NEW,
ENQUEUED,
SENT,
SUCCEEDED,
FAILED
}
private volatile State state = State.NEW;
private final Lock stateLock = new ReentrantLock();
private final Condition stateCondition = stateLock.newCondition();
public abstract void handleResponse(Executor executor, ResponseListener listener, MadokaMessage mm)
throws MadokaParsingException;
/**
* THis command returns the message to be sent
*
* @return
*/
public abstract byte[] getRequest();
/**
* This is the command number, in the protocol
*
* @return
*/
public abstract int getCommandId();
/**
* Returns current state of the command.
*
* @return current state
*/
public State getState() {
return state;
}
/**
* Sets state of the command.
*
* @param state new state
*/
public void setState(State state) {
stateLock.lock();
try {
this.state = state;
stateCondition.signalAll();
} finally {
stateLock.unlock();
}
}
public boolean awaitStateChange(long timeout, TimeUnit unit, State... expectedStates) throws InterruptedException {
stateLock.lock();
try {
long nanosTimeout = unit.toNanos(timeout);
while (!isInAnyState(expectedStates)) {
if (nanosTimeout <= 0L) {
return false;
}
nanosTimeout = stateCondition.awaitNanos(nanosTimeout);
}
} finally {
stateLock.unlock();
}
return true;
}
private boolean isInAnyState(State[] acceptedStates) {
for (State acceptedState : acceptedStates) {
if (acceptedState == state) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,78 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands;
import java.util.concurrent.Executor;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.bluetooth.daikinmadoka.internal.model.MadokaMessage;
import org.openhab.binding.bluetooth.daikinmadoka.internal.model.MadokaParsingException;
import org.openhab.binding.bluetooth.daikinmadoka.internal.model.MadokaProperties.FanSpeed;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Command used to set the FAN speed for all modes (auto/cool/heat...)
*
* @author Benjamin Lafois - Initial contribution
*
*/
@NonNullByDefault
public class GetFanspeedCommand extends BRC1HCommand {
private final Logger logger = LoggerFactory.getLogger(GetFanspeedCommand.class);
private @Nullable FanSpeed coolingFanSpeed;
private @Nullable FanSpeed heatingFanSpeed;
@Override
public byte[] getRequest() {
return MadokaMessage.createRequest(this);
}
@Override
public void handleResponse(Executor executor, ResponseListener listener, MadokaMessage mm)
throws MadokaParsingException {
byte[] valueCoolingFanSpeed = mm.getValues().get(0x20).getRawValue();
byte[] valueHeatingFanSpeed = mm.getValues().get(0x21).getRawValue();
if (valueCoolingFanSpeed == null || valueHeatingFanSpeed == null) {
setState(State.FAILED);
throw new MadokaParsingException("Incorrect cooling or heating fan speed value");
}
this.coolingFanSpeed = FanSpeed.valueOf(valueCoolingFanSpeed[0]);
this.heatingFanSpeed = FanSpeed.valueOf(valueHeatingFanSpeed[0]);
logger.debug("coolingFanSpeed: {}", coolingFanSpeed);
logger.debug("heatingFanSpeed: {}", heatingFanSpeed);
setState(State.SUCCEEDED);
executor.execute(() -> listener.receivedResponse(this));
}
@Override
public int getCommandId() {
return 80;
}
public @Nullable FanSpeed getCoolingFanSpeed() {
return coolingFanSpeed;
}
public @Nullable FanSpeed getHeatingFanSpeed() {
return heatingFanSpeed;
}
}

View File

@@ -0,0 +1,93 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands;
import java.util.concurrent.Executor;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.bluetooth.daikinmadoka.internal.model.MadokaMessage;
import org.openhab.binding.bluetooth.daikinmadoka.internal.model.MadokaParsingException;
import org.openhab.core.library.types.DecimalType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This command returns the Indoor and Outdoor temperature. Outdoor is not always supported.
*
* @author Benjamin Lafois - Initial contribution
*
*/
@NonNullByDefault
public class GetIndoorOutoorTemperatures extends BRC1HCommand {
private final Logger logger = LoggerFactory.getLogger(GetIndoorOutoorTemperatures.class);
private @Nullable DecimalType indoorTemperature;
private @Nullable DecimalType outdoorTemperature;
@Override
public byte[] getRequest() {
return MadokaMessage.createRequest(this);
}
@Override
public void handleResponse(Executor executor, ResponseListener listener, MadokaMessage mm)
throws MadokaParsingException {
byte[] bIndoorTemperature = mm.getValues().get(0x40).getRawValue();
byte[] bOutdoorTemperature = mm.getValues().get(0x41).getRawValue();
if (bIndoorTemperature == null || bOutdoorTemperature == null) {
setState(State.FAILED);
throw new MadokaParsingException("Incorrect indoor or outdoor temperature");
}
Integer iIndoorTemperature = Integer.valueOf(bIndoorTemperature[0]);
Integer iOutdoorTemperature = Integer.valueOf(bOutdoorTemperature[0]);
if (iOutdoorTemperature == -1) {
iOutdoorTemperature = null;
} else {
if (iOutdoorTemperature < 0) {
iOutdoorTemperature = ((iOutdoorTemperature + 256) - 128) * -1;
}
}
if (iIndoorTemperature != null) {
indoorTemperature = new DecimalType(iIndoorTemperature);
}
if (iOutdoorTemperature != null) {
outdoorTemperature = new DecimalType(iOutdoorTemperature);
}
logger.debug("Indoor Temp: {}", indoorTemperature);
logger.debug("Outdoor Temp: {}", outdoorTemperature);
setState(State.SUCCEEDED);
executor.execute(() -> listener.receivedResponse(this));
}
public @Nullable DecimalType getIndoorTemperature() {
return indoorTemperature;
}
public @Nullable DecimalType getOutdoorTemperature() {
return outdoorTemperature;
}
@Override
public int getCommandId() {
return 272;
}
}

View File

@@ -0,0 +1,68 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands;
import java.util.concurrent.Executor;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.bluetooth.daikinmadoka.internal.model.MadokaMessage;
import org.openhab.binding.bluetooth.daikinmadoka.internal.model.MadokaParsingException;
import org.openhab.binding.bluetooth.daikinmadoka.internal.model.MadokaProperties.OperationMode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This command returns the current AC operation mode
*
* @author Benjamin Lafois - Initial contribution
*
*/
@NonNullByDefault
public class GetOperationmodeCommand extends BRC1HCommand {
private final Logger logger = LoggerFactory.getLogger(GetOperationmodeCommand.class);
private @Nullable OperationMode operationMode;
@Override
public byte[] getRequest() {
return MadokaMessage.createRequest(this);
}
@Override
public void handleResponse(Executor executor, ResponseListener listener, MadokaMessage mm)
throws MadokaParsingException {
byte[] bOperationMode = mm.getValues().get(0x20).getRawValue();
if (bOperationMode == null) {
setState(State.FAILED);
throw new MadokaParsingException("Incorrect operation mode");
}
operationMode = OperationMode.valueOf(bOperationMode[0]);
logger.debug("operationMode: {}", operationMode);
setState(State.SUCCEEDED);
executor.execute(() -> listener.receivedResponse(this));
}
@Override
public int getCommandId() {
return 48;
}
public @Nullable OperationMode getOperationMode() {
return operationMode;
}
}

View File

@@ -0,0 +1,68 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands;
import java.util.concurrent.Executor;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.bluetooth.daikinmadoka.internal.model.MadokaMessage;
import org.openhab.binding.bluetooth.daikinmadoka.internal.model.MadokaParsingException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This command returns the current AC power state (on or off)
*
* @author Benjamin Lafois - Initial contribution
*
*/
@NonNullByDefault
public class GetPowerstateCommand extends BRC1HCommand {
private final Logger logger = LoggerFactory.getLogger(GetPowerstateCommand.class);
private @Nullable Boolean powerState;
@Override
public byte[] getRequest() {
return MadokaMessage.createRequest(this);
}
@Override
public void handleResponse(Executor executor, ResponseListener listener, MadokaMessage mm)
throws MadokaParsingException {
byte[] powerStateValue = mm.getValues().get(0x20).getRawValue();
if (powerStateValue == null || powerStateValue.length != 1) {
setState(State.FAILED);
throw new MadokaParsingException("Incorrect value for PowerState");
}
powerState = Integer.valueOf(powerStateValue[0]) == 1;
logger.debug("PowerState: {}", powerState);
setState(State.SUCCEEDED);
executor.execute(() -> listener.receivedResponse(this));
}
@Override
public int getCommandId() {
return 32;
}
public @Nullable Boolean isPowerState() {
return powerState;
}
}

View File

@@ -0,0 +1,77 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands;
import java.util.concurrent.Executor;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.bluetooth.daikinmadoka.internal.model.MadokaMessage;
import org.openhab.binding.bluetooth.daikinmadoka.internal.model.MadokaParsingException;
import org.openhab.core.library.types.DecimalType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This command returns the setpoint, whatever is the current mode.
*
* @author Benjamin Lafois - Initial contribution
*
*/
@NonNullByDefault
public class GetSetpointCommand extends BRC1HCommand {
private final Logger logger = LoggerFactory.getLogger(GetSetpointCommand.class);
private @Nullable DecimalType heatingSetpoint;
private @Nullable DecimalType coolingSetpoint;
@Override
public byte[] getRequest() {
return MadokaMessage.createRequest(this);
}
@Override
public void handleResponse(Executor executor, ResponseListener listener, MadokaMessage mm)
throws MadokaParsingException {
try {
Integer iHeatingSetpoint = (int) (mm.getValues().get(0x21).getComputedValue() / 128.);
Integer iCoolingSetpoint = (int) (mm.getValues().get(0x20).getComputedValue() / 128.);
this.heatingSetpoint = new DecimalType(iHeatingSetpoint);
this.coolingSetpoint = new DecimalType(iCoolingSetpoint);
logger.debug("heatingSetpoint: {}", heatingSetpoint);
logger.debug("coolingSetpoint: {}", coolingSetpoint);
setState(State.SUCCEEDED);
executor.execute(() -> listener.receivedResponse(this));
} catch (Exception e) {
setState(State.FAILED);
throw new MadokaParsingException(e);
}
}
@Override
public int getCommandId() {
return 64;
}
public @Nullable DecimalType getHeatingSetpoint() {
return heatingSetpoint;
}
public @Nullable DecimalType getCoolingSetpoint() {
return coolingSetpoint;
}
}

View File

@@ -0,0 +1,78 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands;
import java.util.concurrent.Executor;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.bluetooth.daikinmadoka.internal.model.MadokaMessage;
import org.openhab.binding.bluetooth.daikinmadoka.internal.model.MadokaParsingException;
/**
* This command returns the firmware version
*
* @author Benjamin Lafois - Initial contribution
*
*/
@NonNullByDefault
public class GetVersionCommand extends BRC1HCommand {
private @Nullable String remoteControllerVersion;
private @Nullable String communicationControllerVersion;
@Override
public byte[] getRequest() {
return MadokaMessage.createRequest(this);
}
@Override
public void handleResponse(Executor executor, ResponseListener listener, MadokaMessage mm)
throws MadokaParsingException {
// In this method, we intentionally do not check for null values in mv45 and mv46. In case of null pointer
// access, it will be catched by the global exception and be reported as a Parsing Reponse exception.
byte[] mv45 = mm.getValues().get(0x45).getRawValue();
byte[] mv46 = mm.getValues().get(0x46).getRawValue();
if (mv45 == null || mv45.length != 3 || mv46 == null || mv46.length != 2) {
setState(State.FAILED);
throw new MadokaParsingException("Incorrect version value");
}
int remoteControllerMajor = mv45[0];
int remoteControllerMinor = mv45[1];
int remoteControllerRevision = mv45[2];
this.remoteControllerVersion = remoteControllerMajor + "." + remoteControllerMinor + "."
+ remoteControllerRevision;
int commControllerMajor = mv46[0];
int commControllerMinor = mv46[1];
this.communicationControllerVersion = commControllerMajor + "." + commControllerMinor;
setState(State.SUCCEEDED);
executor.execute(() -> listener.receivedResponse(this));
}
@Override
public int getCommandId() {
return 304;
}
public @Nullable String getRemoteControllerVersion() {
return remoteControllerVersion;
}
public @Nullable String getCommunicationControllerVersion() {
return communicationControllerVersion;
}
}

View File

@@ -0,0 +1,47 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* Command responses that the listener must implement
*
* @author Benjamin Lafois - Initial contribution
*
*/
@NonNullByDefault
public interface ResponseListener {
public void receivedResponse(byte[] bytes);
public void receivedResponse(GetVersionCommand command);
public void receivedResponse(GetFanspeedCommand command);
public void receivedResponse(GetOperationmodeCommand command);
public void receivedResponse(GetPowerstateCommand command);
public void receivedResponse(GetSetpointCommand command);
public void receivedResponse(GetIndoorOutoorTemperatures command);
public void receivedResponse(SetPowerstateCommand command);
public void receivedResponse(SetSetpointCommand command);
public void receivedResponse(SetOperationmodeCommand command);
public void receivedResponse(SetFanspeedCommand command);
}

View File

@@ -0,0 +1,75 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands;
import java.util.concurrent.Executor;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.bluetooth.daikinmadoka.internal.model.MadokaMessage;
import org.openhab.binding.bluetooth.daikinmadoka.internal.model.MadokaProperties.FanSpeed;
import org.openhab.binding.bluetooth.daikinmadoka.internal.model.MadokaValue;
import org.openhab.core.util.HexUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This command sets the fanspeed for the current mode
*
* @author Benjamin Lafois - Initial contribution
*
*/
@NonNullByDefault
public class SetFanspeedCommand extends BRC1HCommand {
private final Logger logger = LoggerFactory.getLogger(SetFanspeedCommand.class);
private FanSpeed coolingFanSpeed;
private FanSpeed heatingFanSpeed;
public SetFanspeedCommand(FanSpeed coolingFanSpeed, FanSpeed heatingFanSpeed) {
this.coolingFanSpeed = coolingFanSpeed;
this.heatingFanSpeed = heatingFanSpeed;
}
@Override
public byte[] getRequest() {
MadokaValue paramCoolingFanSpeed = new MadokaValue(0x20, 1, new byte[] { (byte) coolingFanSpeed.value() });
MadokaValue paramHeatingFanSpeed = new MadokaValue(0x21, 1, new byte[] { (byte) heatingFanSpeed.value() });
return MadokaMessage.createRequest(this, paramCoolingFanSpeed, paramHeatingFanSpeed);
}
@Override
public void handleResponse(Executor executor, ResponseListener listener, MadokaMessage mm) {
byte[] msg = mm.getRawMessage();
if (logger.isDebugEnabled() && msg != null) {
logger.debug("Got response for {} : {}", this.getClass().getSimpleName(), HexUtils.bytesToHex(msg));
}
setState(State.SUCCEEDED);
executor.execute(() -> listener.receivedResponse(this));
}
@Override
public int getCommandId() {
return 16464;
}
public FanSpeed getCoolingFanSpeed() {
return coolingFanSpeed;
}
public FanSpeed getHeatingFanSpeed() {
return heatingFanSpeed;
}
}

View File

@@ -0,0 +1,67 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands;
import java.util.concurrent.Executor;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.bluetooth.daikinmadoka.internal.model.MadokaMessage;
import org.openhab.binding.bluetooth.daikinmadoka.internal.model.MadokaProperties.OperationMode;
import org.openhab.binding.bluetooth.daikinmadoka.internal.model.MadokaValue;
import org.openhab.core.util.HexUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This command is in charge of changing the current operation mode
*
* @author Benjamin Lafois - Initial contribution
*
*/
@NonNullByDefault
public class SetOperationmodeCommand extends BRC1HCommand {
private final Logger logger = LoggerFactory.getLogger(SetOperationmodeCommand.class);
private OperationMode operationMode;
public SetOperationmodeCommand(OperationMode operationMode) {
this.operationMode = operationMode;
}
@Override
public byte[] getRequest() {
MadokaValue mv = new MadokaValue(0x20, 1, new byte[] { (byte) this.operationMode.value() });
return MadokaMessage.createRequest(this, mv);
}
@Override
public void handleResponse(Executor executor, ResponseListener listener, MadokaMessage mm) {
byte[] msg = mm.getRawMessage();
if (logger.isDebugEnabled() && msg != null) {
logger.debug("Got response for {} : {}", this.getClass().getSimpleName(), HexUtils.bytesToHex(msg));
}
setState(State.SUCCEEDED);
executor.execute(() -> listener.receivedResponse(this));
}
@Override
public int getCommandId() {
return 16432;
}
public OperationMode getOperationMode() {
return operationMode;
}
}

View File

@@ -0,0 +1,69 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands;
import java.util.concurrent.Executor;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.bluetooth.daikinmadoka.internal.model.MadokaMessage;
import org.openhab.binding.bluetooth.daikinmadoka.internal.model.MadokaValue;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.util.HexUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This command is in charge of turning on or off the AC.
*
* @author Benjamin Lafois - Initial contribution
*
*/
@NonNullByDefault
public class SetPowerstateCommand extends BRC1HCommand {
private final Logger logger = LoggerFactory.getLogger(SetPowerstateCommand.class);
private OnOffType powerState;
public SetPowerstateCommand(OnOffType powerState) {
this.powerState = powerState;
}
@Override
public byte[] getRequest() {
MadokaValue mv = new MadokaValue(0x20, 1,
new byte[] { (byte) (this.powerState == OnOffType.ON ? 0x01 : 0x00) });
return MadokaMessage.createRequest(this, mv);
}
@Override
public void handleResponse(Executor executor, ResponseListener listener, MadokaMessage mm) {
byte[] msg = mm.getRawMessage();
if (logger.isDebugEnabled() && msg != null) {
logger.debug("Got response for {} : {}", this.getClass().getSimpleName(), HexUtils.bytesToHex(msg));
}
setState(State.SUCCEEDED);
executor.execute(() -> listener.receivedResponse(this));
}
@Override
public int getCommandId() {
return 16416;
}
public OnOffType getPowerState() {
return powerState;
}
}

View File

@@ -0,0 +1,82 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.bluetooth.daikinmadoka.internal.model.commands;
import java.nio.ByteBuffer;
import java.util.concurrent.Executor;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.bluetooth.daikinmadoka.internal.model.MadokaMessage;
import org.openhab.binding.bluetooth.daikinmadoka.internal.model.MadokaValue;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.util.HexUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* THis command is in charge of changing the AC setpoint
*
* @author Benjamin Lafois - Initial contribution
*
*/
@NonNullByDefault
public class SetSetpointCommand extends BRC1HCommand {
private final Logger logger = LoggerFactory.getLogger(SetSetpointCommand.class);
private DecimalType coolingSetpoint;
private DecimalType heatingSetpoint;
public SetSetpointCommand(DecimalType coolingSetpoint, DecimalType heatingSetpoint) {
this.coolingSetpoint = coolingSetpoint;
this.heatingSetpoint = heatingSetpoint;
}
@Override
public byte[] getRequest() {
byte[] heatingSetpointBytes = ByteBuffer.allocate(2).putShort((short) (128. * heatingSetpoint.shortValue()))
.array();
byte[] coolingSetpointBytes = ByteBuffer.allocate(2).putShort((short) (128. * coolingSetpoint.shortValue()))
.array();
MadokaValue mvHeatingSetpoint = new MadokaValue(0x21, 2, heatingSetpointBytes);
MadokaValue mvCoolingSetpoint = new MadokaValue(0x20, 2, coolingSetpointBytes);
return MadokaMessage.createRequest(this, mvCoolingSetpoint, mvHeatingSetpoint);
}
@Override
public void handleResponse(Executor executor, ResponseListener listener, MadokaMessage mm) {
byte[] msg = mm.getRawMessage();
if (logger.isDebugEnabled() && msg != null) {
logger.debug("Got response for {} : {}", this.getClass().getSimpleName(), HexUtils.bytesToHex(msg));
}
setState(State.SUCCEEDED);
executor.execute(() -> listener.receivedResponse(this));
}
@Override
public int getCommandId() {
return 16448;
}
public DecimalType getCoolingSetpoint() {
return coolingSetpoint;
}
public DecimalType getHeatingSetpoint() {
return heatingSetpoint;
}
}

View File

@@ -0,0 +1,133 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="bluetooth"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="brc1h">
<supported-bridge-type-refs>
<bridge-type-ref id="dbusbluez"/>
</supported-bridge-type-refs>
<label>Daikin BRC1H Thermostat</label>
<description>A Daikin Madoka BRC1H Thermostat (BLE)</description>
<channels>
<channel id="rssi" typeId="rssi"/>
<channel id="onOffStatus" typeId="brc1h_onOffStatus"/>
<channel id="indoorTemperature" typeId="brc1h_indoorTemperature"/>
<channel id="outdoorTemperature" typeId="brc1h_outdoorTemperature"/>
<channel id="remoteCtrlVersion" typeId="brc1h_remoteCtrlVersion"/>
<channel id="commCtrlVersion" typeId="brc1h_commCtrlVersion"/>
<channel id="operationMode" typeId="brc1h_operationMode"/>
<channel id="setpoint" typeId="brc1h_setpoint"/>
<channel id="fanSpeed" typeId="brc1h_fanSpeed"/>
<channel id="homekitCurrentHeatingCoolingMode" typeId="brc1h_homekitCurrentHeatingCoolingMode"/>
<channel id="homekitTargetHeatingCoolingMode" typeId="brc1h_homekitTargetHeatingCoolingMode"/>
<channel id="homebridgeMode" typeId="brc1h_homebridgeMode"/>
</channels>
<config-description>
<parameter name="address" type="text" required="true">
<label>Address</label>
<description>Bluetooth address in XX:XX:XX:XX:XX:XX format</description>
</parameter>
<parameter name="refreshInterval" type="integer" min="1" max="86400" unit="s">
<label>Refresh Interval</label>
<advanced>true</advanced>
<description>Refresh interval for battery and light sensor data (in seconds). This could impact battery lifetime</description>
<default>60</default>
</parameter>
<parameter name="commandTimeout" type="integer" unit="ms">
<label>Command Timeout</label>
<advanced>true</advanced>
<description>The amount of time, in milliseconds, a command should take before it times out.</description>
<default>1000</default>
</parameter>
</config-description>
</thing-type>
<channel-type id="brc1h_onOffStatus">
<item-type>Switch</item-type>
<label>Unit Power Status</label>
</channel-type>
<channel-type id="brc1h_indoorTemperature">
<item-type>Number:Temperature</item-type>
<label>Indoor Temperature</label>
</channel-type>
<channel-type id="brc1h_outdoorTemperature">
<item-type>Number:Temperature</item-type>
<label>Outdoor Temperature</label>
</channel-type>
<channel-type id="brc1h_remoteCtrlVersion">
<item-type>String</item-type>
<label>Remote Controller Version</label>
</channel-type>
<channel-type id="brc1h_commCtrlVersion">
<item-type>String</item-type>
<label>Communication Controller Version</label>
</channel-type>
<channel-type id="brc1h_operationMode">
<item-type>String</item-type>
<label>Operation Mode</label>
<command>
<options>
<option value="FAN">Fan</option>
<option value="DRY">Dry</option>
<option value="AUTO">Auto</option>
<option value="COOL">Cool</option>
<option value="HEAT">Heat</option>
<option value="VENTILATION">Ventilation</option>
</options>
</command>
</channel-type>
<channel-type id="brc1h_setpoint">
<item-type>Number:Temperature</item-type>
<label>Setpoint</label>
</channel-type>
<channel-type id="brc1h_fanSpeed">
<item-type>Number</item-type>
<label>Fan Speed</label>
</channel-type>
<channel-type id="brc1h_homekitCurrentHeatingCoolingMode">
<item-type>String</item-type>
<label>HomeKit CurrentMode</label>
<description>Readonly value. Off, Heating, Cooling, Auto</description>
</channel-type>
<channel-type id="brc1h_homekitTargetHeatingCoolingMode">
<item-type>String</item-type>
<label>HomeKit Target Mode</label>
<command>
<options>
<option value="Off">Off</option>
<option value="CoolOn">Cool</option>
<option value="HeatOn">Heat</option>
<option value="Auto">Auto</option>
</options>
</command>
</channel-type>
<channel-type id="brc1h_homebridgeMode">
<item-type>Number</item-type>
<label>Mode from Homebridge</label>
<command>
<options>
<option value="0">Off</option>
<option value="1">Heating</option>
<option value="2">Cooling</option>
<option value="3">Auto</option>
</options>
</command>
</channel-type>
</thing:thing-descriptions>