added migrated 2.x add-ons
Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<features name="org.openhab.binding.danfossairunit-${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-danfossairunit" description="DanfossAirUnit Binding" version="${project.version}">
|
||||
<feature>openhab-runtime-base</feature>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.danfossairunit/${project.version}</bundle>
|
||||
</feature>
|
||||
</features>
|
||||
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* 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.danfossairunit.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* This enum holds the available channels with their properties (name, ...) and read/write accessors to access
|
||||
* the corresponding values on the air unit.
|
||||
*
|
||||
* @author Robert Bach - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public enum Channel {
|
||||
|
||||
// Main Channels
|
||||
|
||||
CHANNEL_CURRENT_TIME("current_time", ChannelGroup.MAIN, DanfossAirUnit::getCurrentTime),
|
||||
CHANNEL_MODE("mode", ChannelGroup.MAIN, DanfossAirUnit::getMode, DanfossAirUnit::setMode),
|
||||
CHANNEL_MANUAL_FAN_SPEED("manual_fan_speed", ChannelGroup.MAIN, DanfossAirUnit::getManualFanSpeed,
|
||||
DanfossAirUnit::setManualFanSpeed),
|
||||
CHANNEL_EXTRACT_FAN_SPEED("extract_fan_speed", ChannelGroup.MAIN, DanfossAirUnit::getExtractFanSpeed),
|
||||
CHANNEL_SUPPLY_FAN_SPEED("supply_fan_speed", ChannelGroup.MAIN, DanfossAirUnit::getSupplyFanSpeed),
|
||||
CHANNEL_EXTRACT_FAN_STEP("extract_fan_step", ChannelGroup.MAIN, DanfossAirUnit::getExtractFanStep),
|
||||
CHANNEL_SUPPLY_FAN_STEP("supply_fan_step", ChannelGroup.MAIN, DanfossAirUnit::getSupplyFanStep),
|
||||
|
||||
CHANNEL_BOOST("boost", ChannelGroup.MAIN, DanfossAirUnit::getBoost, DanfossAirUnit::setBoost),
|
||||
CHANNEL_NIGHT_COOLING("night_cooling", ChannelGroup.MAIN, DanfossAirUnit::getNightCooling,
|
||||
DanfossAirUnit::setNightCooling),
|
||||
|
||||
// Main Temperature Channels
|
||||
CHANNEL_ROOM_TEMP("room_temp", ChannelGroup.TEMPS, DanfossAirUnit::getRoomTemperature),
|
||||
CHANNEL_ROOM_TEMP_CALCULATED("room_temp_calculated", ChannelGroup.TEMPS,
|
||||
DanfossAirUnit::getRoomTemperatureCalculated),
|
||||
CHANNEL_OUTDOOR_TEMP("outdoor_temp", ChannelGroup.TEMPS, DanfossAirUnit::getOutdoorTemperature),
|
||||
|
||||
// Humidity Channel
|
||||
CHANNEL_HUMIDITY("humidity", ChannelGroup.HUMIDITY, DanfossAirUnit::getHumidity),
|
||||
|
||||
// recuperator channels
|
||||
CHANNEL_BYPASS("bypass", ChannelGroup.RECUPERATOR, DanfossAirUnit::getBypass, DanfossAirUnit::setBypass),
|
||||
CHANNEL_SUPPLY_TEMP("supply_temp", ChannelGroup.RECUPERATOR, DanfossAirUnit::getSupplyTemperature),
|
||||
CHANNEL_EXTRACT_TEMP("extract_temp", ChannelGroup.RECUPERATOR, DanfossAirUnit::getExtractTemperature),
|
||||
CHANNEL_EXHAUST_TEMP("exhaust_temp", ChannelGroup.RECUPERATOR, DanfossAirUnit::getExhaustTemperature),
|
||||
|
||||
// service channels
|
||||
CHANNEL_BATTERY_LIFE("battery_life", ChannelGroup.SERVICE, DanfossAirUnit::getBatteryLife),
|
||||
CHANNEL_FILTER_LIFE("filter_life", ChannelGroup.SERVICE, DanfossAirUnit::getFilterLife);
|
||||
|
||||
private final String channelName;
|
||||
private final ChannelGroup group;
|
||||
private final DanfossAirUnitReadAccessor readAccessor;
|
||||
@Nullable
|
||||
private final DanfossAirUnitWriteAccessor writeAccessor;
|
||||
|
||||
static Channel getByName(String name) {
|
||||
for (Channel channel : values()) {
|
||||
if (channel.getChannelName().equals(name)) {
|
||||
return channel;
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException(String.format("Unknown channel name: %s", name));
|
||||
}
|
||||
|
||||
Channel(String channelName, ChannelGroup group, DanfossAirUnitReadAccessor readAccessor) {
|
||||
this(channelName, group, readAccessor, null);
|
||||
}
|
||||
|
||||
Channel(String channelName, ChannelGroup group, DanfossAirUnitReadAccessor readAccessor,
|
||||
@Nullable DanfossAirUnitWriteAccessor writeAccessor) {
|
||||
this.channelName = channelName;
|
||||
this.group = group;
|
||||
this.readAccessor = readAccessor;
|
||||
this.writeAccessor = writeAccessor;
|
||||
}
|
||||
|
||||
public String getChannelName() {
|
||||
return channelName;
|
||||
}
|
||||
|
||||
public ChannelGroup getGroup() {
|
||||
return group;
|
||||
}
|
||||
|
||||
public DanfossAirUnitReadAccessor getReadAccessor() {
|
||||
return readAccessor;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public DanfossAirUnitWriteAccessor getWriteAccessor() {
|
||||
return writeAccessor;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* 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.danfossairunit.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Represents a channel group, channels are divided into.
|
||||
*
|
||||
* @author Robert Bach - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public enum ChannelGroup {
|
||||
MAIN("main"),
|
||||
TEMPS("temps"),
|
||||
HUMIDITY("humidity"),
|
||||
RECUPERATOR("recuperator"),
|
||||
SERVICE("service");
|
||||
|
||||
private final String groupName;
|
||||
|
||||
ChannelGroup(String groupName) {
|
||||
this.groupName = groupName;
|
||||
}
|
||||
|
||||
public String getGroupName() {
|
||||
return groupName;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* 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.danfossairunit.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* The {@link Commands} interface holds the commands which can be send to the air unit to read/write values or trigger
|
||||
* actions.
|
||||
*
|
||||
* @author Robert Bach - Initial contribution
|
||||
*/
|
||||
|
||||
@NonNullByDefault
|
||||
public class Commands {
|
||||
|
||||
public static byte[] DISCOVER_SEND = { 0x0c, 0x00, 0x30, 0x00, 0x11, 0x00, 0x12, 0x00, 0x13 };
|
||||
public static byte[] DISCOVER_RECEIVE = { 0x0d, 0x00, 0x07, 0x00, 0x02, 0x02, 0x00 };
|
||||
public static byte[] EMPTY = {};
|
||||
public static byte[] GET_HISTORY = { 0x00, 0x30 };
|
||||
public static byte[] REGISTER_0_READ = { 0x00, 0x04 };
|
||||
public static byte[] REGISTER_1_READ = { 0x01, 0x04 };
|
||||
public static byte[] REGISTER_1_WRITE = { 0x01, 0x06 };
|
||||
public static byte[] REGISTER_2_READ = { 0x02, 0x04 };
|
||||
public static byte[] REGISTER_4_READ = { 0x04, 0x04 };
|
||||
public static byte[] REGISTER_6_READ = { 0x06, 0x04 };
|
||||
public static byte[] MODE = { 0x14, 0x12 };
|
||||
public static byte[] MANUAL_FAN_SPEED_STEP = { 0x15, 0x61 };
|
||||
public static byte[] SUPPLY_FAN_SPEED = { 0x14, 0x50 };
|
||||
public static byte[] EXTRACT_FAN_SPEED = { 0x14, 0x51 };
|
||||
public static byte[] SUPPLY_FAN_STEP = { 0x14, 0x28 };
|
||||
public static byte[] EXTRACT_FAN_STEP = { 0x14, 0x29 };
|
||||
public static byte[] BASE_IN = { 0x14, 0x40 };
|
||||
public static byte[] BASE_OUT = { 0x14, 0x41 };
|
||||
public static byte[] BYPASS = { 0x14, 0x60 };
|
||||
public static byte[] BYPASS_DEACTIVATION = { 0x14, 0x63 };
|
||||
public static byte[] BOOST = { 0x15, 0x30 };
|
||||
public static byte[] NIGHT_COOLING = { 0x15, 0x71 };
|
||||
public static byte[] AUTOMATIC_BYPASS = { 0x17, 0x06 };
|
||||
public static byte[] AUTOMATIC_RUSH_AIRING = { 0x17, 0x02 };
|
||||
public static byte[] HUMIDITY = { 0x14, 0x70 };
|
||||
public static byte[] ROOM_TEMPERATURE = { 0x03, 0x00 };
|
||||
public static byte[] ROOM_TEMPERATURE_CALCULATED = { 0x14, (byte) 0x96 };
|
||||
public static byte[] OUTDOOR_TEMPERATURE = { 0x03, 0x34 };
|
||||
public static byte[] SUPPLY_TEMPERATURE = { 0x14, 0x73 };
|
||||
public static byte[] EXTRACT_TEMPERATURE = { 0x14, 0x74 };
|
||||
public static byte[] EXHAUST_TEMPERATURE = { 0x14, 0x75 };
|
||||
public static byte[] BATTERY_LIFE = { 0x03, 0x0f };
|
||||
public static byte[] FILTER_LIFE = { 0x14, 0x6a };
|
||||
public static byte[] CURRENT_TIME = { 0x15, (byte) 0xe0 };
|
||||
public static byte[] AWAY_TO = { 0x15, 0x20 };
|
||||
public static byte[] AWAY_FROM = { 0x15, 0x21 };
|
||||
public static byte[] UNIT_SERIAL = { 0x00, 0x25 }; // endpoint 4
|
||||
public static byte[] UNIT_NAME = { 0x15, (byte) 0xe5 }; // endpoint 1
|
||||
public static byte[] CCM_SERIAL = { 0x14, 0x6a }; // endpoint 0
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
/**
|
||||
* 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.danfossairunit.internal;
|
||||
|
||||
import static org.openhab.binding.danfossairunit.internal.Commands.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.net.InetAddress;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.DateTimeException;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
|
||||
import javax.measure.quantity.Temperature;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.library.types.DateTimeType;
|
||||
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.library.unit.SIUnits;
|
||||
import org.openhab.core.types.Command;
|
||||
|
||||
/**
|
||||
* The {@link DanfossAirUnit} class represents the air unit device and build the commands to be sent by
|
||||
* {@link DanfossAirUnitCommunicationController}
|
||||
*
|
||||
* @author Ralf Duckstein - Initial contribution
|
||||
* @author Robert Bach - heavy refactorings
|
||||
*/
|
||||
|
||||
@SuppressWarnings("SameParameterValue")
|
||||
@NonNullByDefault
|
||||
public class DanfossAirUnit {
|
||||
|
||||
private final DanfossAirUnitCommunicationController communicationController;
|
||||
|
||||
public DanfossAirUnit(InetAddress inetAddr, int port) {
|
||||
this.communicationController = new DanfossAirUnitCommunicationController(inetAddr, port);
|
||||
}
|
||||
|
||||
public void cleanUp() {
|
||||
this.communicationController.disconnect();
|
||||
}
|
||||
|
||||
private boolean getBoolean(byte[] operation, byte[] register) throws IOException {
|
||||
return communicationController.sendRobustRequest(operation, register)[0] != 0;
|
||||
}
|
||||
|
||||
private void setSetting(byte[] register, boolean value) throws IOException {
|
||||
setSetting(register, value ? (byte) 1 : (byte) 0);
|
||||
}
|
||||
|
||||
private short getWord(byte[] operation, byte[] register) throws IOException {
|
||||
byte[] resultBytes = communicationController.sendRobustRequest(operation, register);
|
||||
return (short) ((resultBytes[0] << 8) | (resultBytes[1] & 0xFF));
|
||||
}
|
||||
|
||||
private byte getByte(byte[] operation, byte[] register) throws IOException {
|
||||
return communicationController.sendRobustRequest(operation, register)[0];
|
||||
}
|
||||
|
||||
private String getString(byte[] operation, byte[] register) throws IOException {
|
||||
// length of the string is stored in the first byte
|
||||
byte[] result = communicationController.sendRobustRequest(operation, register);
|
||||
return new String(result, 1, result[0], StandardCharsets.US_ASCII);
|
||||
}
|
||||
|
||||
private void set(byte[] operation, byte[] register, byte value) throws IOException {
|
||||
byte[] valueArray = { value };
|
||||
communicationController.sendRobustRequest(operation, register, valueArray);
|
||||
}
|
||||
|
||||
private void set(byte[] operation, byte[] register, short value) throws IOException {
|
||||
communicationController.sendRobustRequest(operation, register, shortToBytes(value));
|
||||
}
|
||||
|
||||
private byte[] shortToBytes(short s) {
|
||||
return new byte[] { (byte) ((s & 0xFF00) >> 8), (byte) (s & 0x00FF) };
|
||||
}
|
||||
|
||||
private short getShort(byte[] operation, byte[] register) throws IOException {
|
||||
byte[] result = communicationController.sendRobustRequest(operation, register);
|
||||
return (short) ((result[0] << 8) + (result[1] & 0xff));
|
||||
}
|
||||
|
||||
private float getTemperature(byte[] operation, byte[] register)
|
||||
throws IOException, UnexpectedResponseValueException {
|
||||
short shortTemp = getShort(operation, register);
|
||||
float temp = ((float) shortTemp) / 100;
|
||||
if (temp <= -274 || temp > 100) {
|
||||
throw new UnexpectedResponseValueException(String.format("Invalid temperature: %s", temp));
|
||||
}
|
||||
return temp;
|
||||
}
|
||||
|
||||
private ZonedDateTime getTimestamp(byte[] operation, byte[] register)
|
||||
throws IOException, UnexpectedResponseValueException {
|
||||
byte[] result = communicationController.sendRobustRequest(operation, register);
|
||||
return asZonedDateTime(result);
|
||||
}
|
||||
|
||||
private ZonedDateTime asZonedDateTime(byte[] data) throws UnexpectedResponseValueException {
|
||||
int second = data[0];
|
||||
int minute = data[1];
|
||||
int hour = data[2] & 0x1f;
|
||||
int day = data[3] & 0x1f;
|
||||
int month = data[4];
|
||||
int year = data[5] + 2000;
|
||||
try {
|
||||
return ZonedDateTime.of(year, month, day, hour, minute, second, 0, ZoneId.systemDefault());
|
||||
} catch (DateTimeException e) {
|
||||
String msg = String.format("Ignoring invalid timestamp %s.%s.%s %s:%s:%s", day, month, year, hour, minute,
|
||||
second);
|
||||
throw new UnexpectedResponseValueException(msg, e);
|
||||
}
|
||||
}
|
||||
|
||||
private static int asUnsignedByte(byte b) {
|
||||
return b & 0xFF;
|
||||
}
|
||||
|
||||
private static float asPercentByte(byte b) {
|
||||
float f = asUnsignedByte(b);
|
||||
return f * 100 / 255;
|
||||
}
|
||||
|
||||
private void setSetting(byte[] register, short value) throws IOException {
|
||||
byte[] valueArray = new byte[2];
|
||||
valueArray[0] = (byte) (value >> 8);
|
||||
valueArray[1] = (byte) value;
|
||||
|
||||
communicationController.sendRobustRequest(REGISTER_1_WRITE, register, valueArray);
|
||||
}
|
||||
|
||||
public String getUnitName() throws IOException {
|
||||
return getString(REGISTER_1_READ, UNIT_NAME);
|
||||
}
|
||||
|
||||
public String getUnitSerialNumber() throws IOException {
|
||||
return String.valueOf(getShort(REGISTER_4_READ, UNIT_SERIAL));
|
||||
}
|
||||
|
||||
public StringType getMode() throws IOException {
|
||||
return new StringType(Mode.values()[getByte(REGISTER_1_READ, MODE)].name());
|
||||
}
|
||||
|
||||
public PercentType getManualFanSpeed() throws IOException {
|
||||
return new PercentType(BigDecimal.valueOf(getByte(REGISTER_1_READ, MANUAL_FAN_SPEED_STEP) * 10));
|
||||
}
|
||||
|
||||
public DecimalType getSupplyFanSpeed() throws IOException {
|
||||
return new DecimalType(BigDecimal.valueOf(getWord(REGISTER_4_READ, SUPPLY_FAN_SPEED)));
|
||||
}
|
||||
|
||||
public DecimalType getExtractFanSpeed() throws IOException {
|
||||
return new DecimalType(BigDecimal.valueOf(getWord(REGISTER_4_READ, EXTRACT_FAN_SPEED)));
|
||||
}
|
||||
|
||||
public PercentType getSupplyFanStep() throws IOException {
|
||||
return new PercentType(BigDecimal.valueOf(getByte(REGISTER_4_READ, SUPPLY_FAN_STEP)));
|
||||
}
|
||||
|
||||
public PercentType getExtractFanStep() throws IOException {
|
||||
return new PercentType(BigDecimal.valueOf(getByte(REGISTER_4_READ, EXTRACT_FAN_STEP)));
|
||||
}
|
||||
|
||||
public OnOffType getBoost() throws IOException {
|
||||
return getBoolean(REGISTER_1_READ, BOOST) ? OnOffType.ON : OnOffType.OFF;
|
||||
}
|
||||
|
||||
public OnOffType getNightCooling() throws IOException {
|
||||
return getBoolean(REGISTER_1_READ, NIGHT_COOLING) ? OnOffType.ON : OnOffType.OFF;
|
||||
}
|
||||
|
||||
public OnOffType getBypass() throws IOException {
|
||||
return getBoolean(REGISTER_1_READ, BYPASS) ? OnOffType.ON : OnOffType.OFF;
|
||||
}
|
||||
|
||||
public DecimalType getHumidity() throws IOException {
|
||||
BigDecimal value = BigDecimal.valueOf(asPercentByte(getByte(REGISTER_1_READ, HUMIDITY)));
|
||||
return new DecimalType(value.setScale(1, RoundingMode.HALF_UP));
|
||||
}
|
||||
|
||||
public QuantityType<Temperature> getRoomTemperature() throws IOException, UnexpectedResponseValueException {
|
||||
return getTemperatureAsDecimalType(REGISTER_1_READ, ROOM_TEMPERATURE);
|
||||
}
|
||||
|
||||
public QuantityType<Temperature> getRoomTemperatureCalculated()
|
||||
throws IOException, UnexpectedResponseValueException {
|
||||
return getTemperatureAsDecimalType(REGISTER_0_READ, ROOM_TEMPERATURE_CALCULATED);
|
||||
}
|
||||
|
||||
public QuantityType<Temperature> getOutdoorTemperature() throws IOException, UnexpectedResponseValueException {
|
||||
return getTemperatureAsDecimalType(REGISTER_1_READ, OUTDOOR_TEMPERATURE);
|
||||
}
|
||||
|
||||
public QuantityType<Temperature> getSupplyTemperature() throws IOException, UnexpectedResponseValueException {
|
||||
return getTemperatureAsDecimalType(REGISTER_4_READ, SUPPLY_TEMPERATURE);
|
||||
}
|
||||
|
||||
public QuantityType<Temperature> getExtractTemperature() throws IOException, UnexpectedResponseValueException {
|
||||
return getTemperatureAsDecimalType(REGISTER_4_READ, EXTRACT_TEMPERATURE);
|
||||
}
|
||||
|
||||
public QuantityType<Temperature> getExhaustTemperature() throws IOException, UnexpectedResponseValueException {
|
||||
return getTemperatureAsDecimalType(REGISTER_4_READ, EXHAUST_TEMPERATURE);
|
||||
}
|
||||
|
||||
private QuantityType<Temperature> getTemperatureAsDecimalType(byte[] operation, byte[] register)
|
||||
throws IOException, UnexpectedResponseValueException {
|
||||
BigDecimal value = BigDecimal.valueOf(getTemperature(operation, register));
|
||||
return new QuantityType<>(value.setScale(1, RoundingMode.HALF_UP), SIUnits.CELSIUS);
|
||||
}
|
||||
|
||||
public DecimalType getBatteryLife() throws IOException {
|
||||
return new DecimalType(BigDecimal.valueOf(asUnsignedByte(getByte(REGISTER_1_READ, BATTERY_LIFE))));
|
||||
}
|
||||
|
||||
public DecimalType getFilterLife() throws IOException {
|
||||
return new DecimalType(BigDecimal.valueOf(asPercentByte(getByte(REGISTER_1_READ, FILTER_LIFE))));
|
||||
}
|
||||
|
||||
public DateTimeType getCurrentTime() throws IOException, UnexpectedResponseValueException {
|
||||
ZonedDateTime timestamp = getTimestamp(REGISTER_1_READ, CURRENT_TIME);
|
||||
return new DateTimeType(timestamp);
|
||||
}
|
||||
|
||||
public PercentType setManualFanSpeed(Command cmd) throws IOException {
|
||||
return setPercentTypeRegister(cmd, MANUAL_FAN_SPEED_STEP);
|
||||
}
|
||||
|
||||
private PercentType setPercentTypeRegister(Command cmd, byte[] register) throws IOException {
|
||||
if (cmd instanceof PercentType) {
|
||||
byte value = (byte) ((((PercentType) cmd).intValue() + 5) / 10);
|
||||
set(REGISTER_1_WRITE, register, value);
|
||||
}
|
||||
return new PercentType(BigDecimal.valueOf(getByte(REGISTER_1_READ, register) * 10));
|
||||
}
|
||||
|
||||
private OnOffType setOnOffTypeRegister(Command cmd, byte[] register) throws IOException {
|
||||
if (cmd instanceof OnOffType) {
|
||||
set(REGISTER_1_WRITE, register, OnOffType.ON.equals(cmd) ? (byte) 1 : (byte) 0);
|
||||
}
|
||||
return getBoolean(REGISTER_1_READ, register) ? OnOffType.ON : OnOffType.OFF;
|
||||
}
|
||||
|
||||
private StringType setStringTypeRegister(Command cmd, byte[] register) throws IOException {
|
||||
if (cmd instanceof StringType) {
|
||||
byte value = (byte) (Mode.valueOf(cmd.toString()).ordinal());
|
||||
set(REGISTER_1_WRITE, register, value);
|
||||
}
|
||||
|
||||
return new StringType(Mode.values()[getByte(REGISTER_1_READ, register)].name());
|
||||
}
|
||||
|
||||
public StringType setMode(Command cmd) throws IOException {
|
||||
return setStringTypeRegister(cmd, MODE);
|
||||
}
|
||||
|
||||
public OnOffType setBoost(Command cmd) throws IOException {
|
||||
return setOnOffTypeRegister(cmd, BOOST);
|
||||
}
|
||||
|
||||
public OnOffType setNightCooling(Command cmd) throws IOException {
|
||||
return setOnOffTypeRegister(cmd, NIGHT_COOLING);
|
||||
}
|
||||
|
||||
public OnOffType setBypass(Command cmd) throws IOException {
|
||||
return setOnOffTypeRegister(cmd, BYPASS);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* 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.danfossairunit.internal;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
|
||||
/**
|
||||
* The {@link DanfossAirUnitBindingConstants} class defines common constants, which are
|
||||
* used across the whole binding.
|
||||
*
|
||||
* @author Ralf Duckstein - Initial contribution
|
||||
* @author Robert Bach - heavy refactorings
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class DanfossAirUnitBindingConstants {
|
||||
|
||||
public static String BINDING_ID = "danfossairunit";
|
||||
|
||||
// List of all Thing Type UIDs
|
||||
public static ThingTypeUID THING_TYPE_SAMPLE = new ThingTypeUID(BINDING_ID, "sample");
|
||||
|
||||
// List of all Channel ids
|
||||
public static String CHANNEL_1 = "channel1";
|
||||
|
||||
// The only thing type UIDs
|
||||
public static ThingTypeUID THING_TYPE_AIRUNIT = new ThingTypeUID(BINDING_ID, "airunit");
|
||||
|
||||
// The thing type as a set
|
||||
public static Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections.singleton(THING_TYPE_AIRUNIT);
|
||||
|
||||
// Properties
|
||||
public static String PROPERTY_UNIT_NAME = "Unit Name";
|
||||
public static String PROPERTY_SERIAL = "Serial Number";
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* 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.danfossairunit.internal;
|
||||
|
||||
import static org.openhab.binding.danfossairunit.internal.Commands.EMPTY;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.InetAddress;
|
||||
import java.net.Socket;
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link DanfossAirUnitCommunicationController} class does the actual network communication with the air unit.
|
||||
*
|
||||
* @author Robert Bach - initial contribution
|
||||
*/
|
||||
|
||||
@NonNullByDefault
|
||||
public class DanfossAirUnitCommunicationController {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(DanfossAirUnitCommunicationController.class);
|
||||
|
||||
private final InetAddress inetAddr;
|
||||
private final int port;
|
||||
private boolean connected = false;
|
||||
private @Nullable Socket socket;
|
||||
private @Nullable OutputStream oStream;
|
||||
private @Nullable InputStream iStream;
|
||||
|
||||
public DanfossAirUnitCommunicationController(InetAddress inetAddr, int port) {
|
||||
this.inetAddr = inetAddr;
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
public synchronized void connect() throws IOException {
|
||||
if (connected) {
|
||||
return;
|
||||
}
|
||||
socket = new Socket(inetAddr, port);
|
||||
oStream = socket.getOutputStream();
|
||||
iStream = socket.getInputStream();
|
||||
connected = true;
|
||||
}
|
||||
|
||||
public synchronized void disconnect() {
|
||||
if (!connected) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (socket != null) {
|
||||
socket.close();
|
||||
}
|
||||
} catch (IOException ioe) {
|
||||
logger.debug("Connection to air unit could not be closed gracefully. {}", ioe.getMessage());
|
||||
} finally {
|
||||
socket = null;
|
||||
iStream = null;
|
||||
oStream = null;
|
||||
}
|
||||
connected = false;
|
||||
}
|
||||
|
||||
public byte[] sendRobustRequest(byte[] operation, byte[] register) throws IOException {
|
||||
return sendRobustRequest(operation, register, EMPTY);
|
||||
}
|
||||
|
||||
public synchronized byte[] sendRobustRequest(byte[] operation, byte[] register, byte[] value) throws IOException {
|
||||
connect();
|
||||
byte[] request = new byte[4 + value.length];
|
||||
System.arraycopy(operation, 0, request, 0, 2);
|
||||
System.arraycopy(register, 0, request, 2, 2);
|
||||
System.arraycopy(value, 0, request, 4, value.length);
|
||||
try {
|
||||
return sendRequestInternal(request);
|
||||
} catch (IOException ioe) {
|
||||
// retry once if there was connection problem
|
||||
disconnect();
|
||||
connect();
|
||||
return sendRequestInternal(request);
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized byte[] sendRequestInternal(byte[] request) throws IOException {
|
||||
|
||||
if (oStream == null) {
|
||||
throw new IOException(
|
||||
String.format("Output stream is null while sending request: %s", Arrays.toString(request)));
|
||||
}
|
||||
oStream.write(request);
|
||||
oStream.flush();
|
||||
|
||||
byte[] result = new byte[63];
|
||||
if (iStream == null) {
|
||||
throw new IOException(
|
||||
String.format("Input stream is null while sending request: %s", Arrays.toString(request)));
|
||||
}
|
||||
// noinspection ResultOfMethodCallIgnored
|
||||
iStream.read(result, 0, 63);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* 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.danfossairunit.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* The {@link DanfossAirUnitConfiguration} class contains configuration parameters for the air unit.
|
||||
*
|
||||
* @author Ralf Duckstein - Initial contribution
|
||||
* @author Robert Bach - heavy refactorings
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class DanfossAirUnitConfiguration {
|
||||
|
||||
public @Nullable String host;
|
||||
|
||||
public int refreshInterval = 10;
|
||||
|
||||
public long updateUnchangedValuesEveryMillis = 60000;
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
/**
|
||||
* 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.danfossairunit.internal;
|
||||
|
||||
import static org.openhab.binding.danfossairunit.internal.DanfossAirUnitBindingConstants.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
import org.openhab.core.thing.binding.BaseThingHandler;
|
||||
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.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link DanfossAirUnitHandler} is responsible for handling commands, which are
|
||||
* sent to one of the channels.
|
||||
*
|
||||
* @author Ralf Duckstein - Initial contribution
|
||||
* @author Robert Bach - heavy refactorings
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class DanfossAirUnitHandler extends BaseThingHandler {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(DanfossAirUnitHandler.class);
|
||||
private @NonNullByDefault({}) DanfossAirUnitConfiguration config;
|
||||
private @Nullable ValueCache valueCache;
|
||||
private @Nullable ScheduledFuture<?> pollingJob;
|
||||
private @Nullable DanfossAirUnit hrv;
|
||||
|
||||
public DanfossAirUnitHandler(Thing thing) {
|
||||
super(thing);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
if (command instanceof RefreshType) {
|
||||
updateAllChannels();
|
||||
} else {
|
||||
try {
|
||||
DanfossAirUnit danfossAirUnit = hrv;
|
||||
if (danfossAirUnit != null) {
|
||||
Channel channel = Channel.getByName(channelUID.getIdWithoutGroup());
|
||||
DanfossAirUnitWriteAccessor writeAccessor = channel.getWriteAccessor();
|
||||
if (writeAccessor != null) {
|
||||
updateState(channelUID, writeAccessor.access(danfossAirUnit, command));
|
||||
}
|
||||
} else {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.NONE,
|
||||
"Air unit connection not initialized.");
|
||||
return;
|
||||
}
|
||||
} catch (IllegalArgumentException e) {
|
||||
logger.debug("Ignoring unknown channel id: {}", channelUID.getIdWithoutGroup(), e);
|
||||
} catch (IOException ioe) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, ioe.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
updateStatus(ThingStatus.UNKNOWN);
|
||||
config = getConfigAs(DanfossAirUnitConfiguration.class);
|
||||
valueCache = new ValueCache(config.updateUnchangedValuesEveryMillis);
|
||||
try {
|
||||
hrv = new DanfossAirUnit(InetAddress.getByName(config.host), 30046);
|
||||
DanfossAirUnit danfossAirUnit = hrv;
|
||||
scheduler.execute(() -> {
|
||||
try {
|
||||
thing.setProperty(PROPERTY_UNIT_NAME, danfossAirUnit.getUnitName());
|
||||
thing.setProperty(PROPERTY_SERIAL, danfossAirUnit.getUnitSerialNumber());
|
||||
pollingJob = scheduler.scheduleWithFixedDelay(this::updateAllChannels, 5, config.refreshInterval,
|
||||
TimeUnit.SECONDS);
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
} catch (IOException e) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage());
|
||||
}
|
||||
});
|
||||
} catch (UnknownHostException e) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
|
||||
"Unknown host: " + config.host);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private void updateAllChannels() {
|
||||
DanfossAirUnit danfossAirUnit = hrv;
|
||||
if (danfossAirUnit != null) {
|
||||
logger.debug("Updating DanfossHRV data '{}'", getThing().getUID());
|
||||
|
||||
for (Channel channel : Channel.values()) {
|
||||
if (Thread.interrupted()) {
|
||||
logger.debug("Polling thread interrupted...");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
updateState(channel.getGroup().getGroupName(), channel.getChannelName(),
|
||||
channel.getReadAccessor().access(danfossAirUnit));
|
||||
} catch (UnexpectedResponseValueException e) {
|
||||
updateState(channel.getGroup().getGroupName(), channel.getChannelName(), UnDefType.UNDEF);
|
||||
logger.debug(
|
||||
"Cannot update channel {}: an unexpected or invalid response has been received from the air unit: {}",
|
||||
channel.getChannelName(), e.getMessage());
|
||||
} catch (IOException e) {
|
||||
updateState(channel.getGroup().getGroupName(), channel.getChannelName(), UnDefType.UNDEF);
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, e.getMessage());
|
||||
logger.debug("Cannot update channel {}: an error occurred retrieving the value: {}",
|
||||
channel.getChannelName(), e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
if (getThing().getStatus() == ThingStatus.OFFLINE) {
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
logger.debug("Disposing Danfoss HRV handler '{}'", getThing().getUID());
|
||||
|
||||
if (pollingJob != null) {
|
||||
pollingJob.cancel(true);
|
||||
pollingJob = null;
|
||||
}
|
||||
|
||||
if (hrv != null) {
|
||||
hrv.cleanUp();
|
||||
hrv = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void updateState(String groupId, String channelId, State state) {
|
||||
if (valueCache.updateValue(channelId, state)) {
|
||||
updateState(new ChannelUID(thing.getUID(), groupId, channelId), state);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* 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.danfossairunit.internal;
|
||||
|
||||
import static org.openhab.binding.danfossairunit.internal.DanfossAirUnitBindingConstants.THING_TYPE_AIRUNIT;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
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;
|
||||
|
||||
/**
|
||||
* The {@link DanfossAirUnitHandlerFactory} is responsible for creating things and thing
|
||||
* handlers.
|
||||
*
|
||||
* @author Ralf Duckstein - Initial contribution
|
||||
* @author Robert Bach - heavy refactorings
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(configurationPid = "binding.danfossairunit", service = ThingHandlerFactory.class)
|
||||
public class DanfossAirUnitHandlerFactory extends BaseThingHandlerFactory {
|
||||
|
||||
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections.singleton(THING_TYPE_AIRUNIT);
|
||||
|
||||
@Override
|
||||
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
|
||||
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @Nullable ThingHandler createHandler(Thing thing) {
|
||||
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
|
||||
|
||||
if (thingTypeUID.equals(THING_TYPE_AIRUNIT)) {
|
||||
return new DanfossAirUnitHandler(thing);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* 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.danfossairunit.internal;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.types.State;
|
||||
|
||||
/**
|
||||
* The {@link DanfossAirUnitReadAccessor} encapsulates access to an air unit value to be read.
|
||||
*
|
||||
* @author Robert Bach - Initial contribution
|
||||
*/
|
||||
@FunctionalInterface
|
||||
@NonNullByDefault
|
||||
public interface DanfossAirUnitReadAccessor {
|
||||
State access(DanfossAirUnit hrv) throws IOException, UnexpectedResponseValueException;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* 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.danfossairunit.internal;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.State;
|
||||
|
||||
/**
|
||||
* The {@link DanfossAirUnitWriteAccessor} encapsulates access to an air unit value to be written.
|
||||
*
|
||||
* @author Robert Bach - Initial contribution
|
||||
*/
|
||||
@FunctionalInterface
|
||||
@NonNullByDefault
|
||||
public interface DanfossAirUnitWriteAccessor {
|
||||
State access(DanfossAirUnit hrv, Command command) throws IOException;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* 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.danfossairunit.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* The {@link Mode} enum represents an air unit operation mode.
|
||||
*
|
||||
* @author Robert Bach - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public enum Mode {
|
||||
DEMAND,
|
||||
PROGRAM,
|
||||
MANUAL,
|
||||
OFF
|
||||
}
|
||||
@@ -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.danfossairunit.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* An exception representing an unexpected value received from the air unit.
|
||||
*
|
||||
* @author Robert Bach - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class UnexpectedResponseValueException extends Exception {
|
||||
|
||||
private static final long serialVersionUID = -5727747058755880978L;
|
||||
|
||||
public UnexpectedResponseValueException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public UnexpectedResponseValueException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* 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.danfossairunit.internal;
|
||||
|
||||
import java.util.Calendar;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.types.State;
|
||||
|
||||
/**
|
||||
* The {@link ValueCache} is responsible for holding the last value of the channels for a
|
||||
* certain amount of time {@link ValueCache#durationMs} to prevent unnecessary event bus updates if the value didn't
|
||||
* change.
|
||||
*
|
||||
* @author Robert Bach - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ValueCache {
|
||||
|
||||
private Map<String, StateWithTimestamp> stateByValue = new HashMap<>();
|
||||
|
||||
private final long durationMs;
|
||||
|
||||
public ValueCache(long durationMs) {
|
||||
this.durationMs = durationMs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates or inserts the given value into the value cache. Returns true if there was no value in the cache
|
||||
* for the given channelId or if the value has updated to a different value or if the value is older than
|
||||
* the cache duration
|
||||
*/
|
||||
public boolean updateValue(String channelId, State newState) {
|
||||
long currentTimeMs = Calendar.getInstance().getTimeInMillis();
|
||||
StateWithTimestamp oldState = stateByValue.get(channelId);
|
||||
boolean writeToCache;
|
||||
if (oldState == null) {
|
||||
writeToCache = true;
|
||||
} else {
|
||||
writeToCache = !oldState.state.equals(newState) || oldState.timestamp < (currentTimeMs - durationMs);
|
||||
}
|
||||
if (writeToCache) {
|
||||
stateByValue.put(channelId, new StateWithTimestamp(newState, currentTimeMs));
|
||||
}
|
||||
return writeToCache;
|
||||
}
|
||||
|
||||
@NonNullByDefault
|
||||
private static class StateWithTimestamp {
|
||||
State state;
|
||||
long timestamp;
|
||||
|
||||
public StateWithTimestamp(State state, long timestamp) {
|
||||
this.state = state;
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* 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.danfossairunit.internal.discovery;
|
||||
|
||||
import static org.openhab.binding.danfossairunit.internal.DanfossAirUnitBindingConstants.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.DatagramPacket;
|
||||
import java.net.DatagramSocket;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InterfaceAddress;
|
||||
import java.net.NetworkInterface;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.util.*;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.config.discovery.AbstractDiscoveryService;
|
||||
import org.openhab.core.config.discovery.DiscoveryResult;
|
||||
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
|
||||
import org.openhab.core.config.discovery.DiscoveryService;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The Discovery service implementation to scan for available air units in the network via broadcast.
|
||||
*
|
||||
* @author Ralf Duckstein - Initial contribution
|
||||
* @author Robert Bach - heavy refactorings
|
||||
*/
|
||||
@Component(service = DiscoveryService.class, immediate = true)
|
||||
@NonNullByDefault
|
||||
public class DanfossAirUnitDiscoveryService extends AbstractDiscoveryService {
|
||||
|
||||
private static final int BROADCAST_PORT = 30045;
|
||||
private static final byte[] DISCOVER_SEND = { 0x0c, 0x00, 0x30, 0x00, 0x11, 0x00, 0x12, 0x00, 0x13 };
|
||||
private static final byte[] DISCOVER_RECEIVE = { 0x0d, 0x00, 0x07, 0x00, 0x02, 0x02, 0x00 };
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(DanfossAirUnitDiscoveryService.class);
|
||||
|
||||
public DanfossAirUnitDiscoveryService() {
|
||||
super(SUPPORTED_THING_TYPES_UIDS, 15, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<ThingTypeUID> getSupportedThingTypes() {
|
||||
return SUPPORTED_THING_TYPES_UIDS;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void startBackgroundDiscovery() {
|
||||
logger.debug("Start Danfoss Air CCM background discovery");
|
||||
scheduler.execute(this::discover);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startScan() {
|
||||
logger.debug("Start Danfoss Air CCM scan");
|
||||
discover();
|
||||
}
|
||||
|
||||
private synchronized void discover() {
|
||||
logger.debug("Try to discover all Danfoss Air CCM devices");
|
||||
|
||||
try (DatagramSocket socket = new DatagramSocket()) {
|
||||
|
||||
Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
|
||||
while (interfaces.hasMoreElements()) {
|
||||
NetworkInterface networkInterface = interfaces.nextElement();
|
||||
if (networkInterface.isLoopback() || !networkInterface.isUp()) {
|
||||
continue;
|
||||
}
|
||||
for (InterfaceAddress interfaceAddress : networkInterface.getInterfaceAddresses()) {
|
||||
if (interfaceAddress.getBroadcast() == null) {
|
||||
continue;
|
||||
}
|
||||
logger.debug("Sending broadcast on interface {} to discover Danfoss Air CCM device...",
|
||||
interfaceAddress.getAddress());
|
||||
sendBroadcastToDiscoverThing(socket, interfaceAddress.getBroadcast());
|
||||
}
|
||||
}
|
||||
|
||||
} catch (IOException e) {
|
||||
logger.debug("No Danfoss Air CCM device found. Diagnostic: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void sendBroadcastToDiscoverThing(DatagramSocket socket, InetAddress broadcastAddress) throws IOException {
|
||||
socket.setBroadcast(true);
|
||||
socket.setSoTimeout(500);
|
||||
// send discover
|
||||
byte[] sendBuffer = DISCOVER_SEND;
|
||||
DatagramPacket sendPacket = new DatagramPacket(sendBuffer, sendBuffer.length, broadcastAddress, BROADCAST_PORT);
|
||||
socket.send(sendPacket);
|
||||
logger.debug("Discover message sent");
|
||||
|
||||
// wait for responses
|
||||
while (true) {
|
||||
byte[] receiveBuffer = new byte[7];
|
||||
DatagramPacket receivePacket = new DatagramPacket(receiveBuffer, receiveBuffer.length);
|
||||
try {
|
||||
socket.receive(receivePacket);
|
||||
} catch (SocketTimeoutException e) {
|
||||
break; // leave the endless loop
|
||||
}
|
||||
|
||||
byte[] data = receivePacket.getData();
|
||||
if (Arrays.equals(data, DISCOVER_RECEIVE)) {
|
||||
logger.debug("Discover received correct response");
|
||||
|
||||
String host = receivePacket.getAddress().getHostName();
|
||||
Map<String, Object> properties = new HashMap<>();
|
||||
properties.put("host", host);
|
||||
|
||||
logger.debug("Adding a new Danfoss Air Unit CCM '{}' to inbox", host);
|
||||
|
||||
ThingUID uid = new ThingUID(THING_TYPE_AIRUNIT, String.valueOf(receivePacket.getAddress().hashCode()));
|
||||
|
||||
DiscoveryResult result = DiscoveryResultBuilder.create(uid).withRepresentationProperty("host")
|
||||
.withProperties(properties).withLabel("Danfoss HRV").build();
|
||||
thingDiscovered(result);
|
||||
|
||||
logger.debug("Thing discovered '{}'", result);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<binding:binding id="danfossairunit" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
|
||||
|
||||
<name>DanfossAirUnit Binding</name>
|
||||
<description>This is the binding for DanfossAirUnit.</description>
|
||||
<author>Ralf Duckstein, Robert Bach</author>
|
||||
|
||||
</binding:binding>
|
||||
@@ -0,0 +1,199 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="danfossairunit"
|
||||
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">
|
||||
|
||||
<!--HRV -->
|
||||
<thing-type id="airunit">
|
||||
<label>Danfoss Air Unit</label>
|
||||
<description>The Danfoss Air Unit Heat Exchanger, CCM and Air Dial</description>
|
||||
|
||||
<channel-groups>
|
||||
<channel-group id="main" typeId="main"/>
|
||||
<channel-group id="temps" typeId="temps"/>
|
||||
<channel-group id="humidity" typeId="humidity"/>
|
||||
<channel-group id="recuperator" typeId="recuperator"/>
|
||||
<channel-group id="service" typeId="service"/>
|
||||
</channel-groups>
|
||||
<properties>
|
||||
<property name="Unit Name">unknown</property>
|
||||
<property name="Serial Number">unknown</property>
|
||||
</properties>
|
||||
<config-description>
|
||||
<parameter name="host" type="text" required="true">
|
||||
<label>Host</label>
|
||||
<context>network-address</context>
|
||||
<description>Host name or IP address of the Danfoss Air CCM</description>
|
||||
</parameter>
|
||||
<parameter name="refreshInterval" type="integer" required="false" unit="s">
|
||||
<default>10</default>
|
||||
<label>Refresh Interval</label>
|
||||
<unitLabel>Seconds</unitLabel>
|
||||
</parameter>
|
||||
<parameter name="updateUnchangedValuesEveryMillis" type="integer" min="0" unit="ms">
|
||||
<label>Interval for Updating Unchanged Values</label>
|
||||
<default>60000</default>
|
||||
<unitLabel>ms</unitLabel>
|
||||
<description>Interval to update unchanged values (to the event bus) in milliseconds. A value of 0 means that every
|
||||
value (received via polling from the air unit) is updated to the event bus, unchanged or not.</description>
|
||||
<advanced>true</advanced>
|
||||
</parameter>
|
||||
</config-description>
|
||||
|
||||
</thing-type>
|
||||
|
||||
<!--Cannel Group Definitions -->
|
||||
<channel-group-type id="main">
|
||||
<label>Mode and Fan Speeds</label>
|
||||
<channels>
|
||||
<channel id="current_time" typeId="currentTime"/>
|
||||
<channel id="mode" typeId="mode"/>
|
||||
<channel id="manual_fan_speed" typeId="manualFanSpeed"/>
|
||||
<channel id="supply_fan_speed" typeId="supplyFanSpeed"/>
|
||||
<channel id="extract_fan_speed" typeId="extractFanSpeed"/>
|
||||
<channel id="supply_fan_step" typeId="supplyFanStep"/>
|
||||
<channel id="extract_fan_step" typeId="extractFanStep"/>
|
||||
<channel id="boost" typeId="switch">
|
||||
<label>Boost</label>
|
||||
<description>Enables fan boost</description>
|
||||
</channel>
|
||||
<channel id="night_cooling" typeId="switch">
|
||||
<label>Night Cooling</label>
|
||||
<description>Enables night cooling</description>
|
||||
</channel>
|
||||
</channels>
|
||||
</channel-group-type>
|
||||
<channel-group-type id="temps">
|
||||
<label>Temperatures</label>
|
||||
<category>Temperature</category>
|
||||
<channels>
|
||||
<channel id="room_temp" typeId="temperature">
|
||||
<label>Room Temperature</label>
|
||||
<description>Temperature of the air in the room of the Air Dial</description>
|
||||
</channel>
|
||||
<channel id="room_temp_calculated" typeId="temperature">
|
||||
<label>Calculated Room Temperature</label>
|
||||
<description>Calculated Room Temperature</description>
|
||||
</channel>
|
||||
<channel id="outdoor_temp" typeId="temperature">
|
||||
<label>Outdoor Temperature</label>
|
||||
<description>Temperature of the air outside</description>
|
||||
</channel>
|
||||
</channels>
|
||||
</channel-group-type>
|
||||
<channel-group-type id="humidity">
|
||||
<label>Humidity</label>
|
||||
<channels>
|
||||
<channel id="humidity" typeId="humidity"/>
|
||||
</channels>
|
||||
</channel-group-type>
|
||||
<channel-group-type id="recuperator" advanced="true">
|
||||
<label>Recuperator</label>
|
||||
<description>Heat exchaning device in the Air Unit</description>
|
||||
<channels>
|
||||
<channel id="bypass" typeId="switch">
|
||||
<label>Bypass</label>
|
||||
<description>Disables the heat exchange. Useful in summer when room temperature is above target and outside
|
||||
temperature is below target.</description>
|
||||
</channel>
|
||||
<channel id="supply_temp" typeId="temperature">
|
||||
<label>Supply Air Temperature</label>
|
||||
<description>Temperature of air which is passed to the rooms</description>
|
||||
</channel>
|
||||
<channel id="extract_temp" typeId="temperature">
|
||||
<label>Extract Air Temperature</label>
|
||||
<description>Temperature of the air as extracted from the rooms</description>
|
||||
</channel>
|
||||
<channel id="exhaust_temp" typeId="temperature">
|
||||
<label>Exhaust Air Temperature</label>
|
||||
<description>Temperature of the air when pushed outside</description>
|
||||
</channel>
|
||||
</channels>
|
||||
</channel-group-type>
|
||||
<channel-group-type id="service" advanced="true">
|
||||
<label>Service</label>
|
||||
<channels>
|
||||
<channel id="battery_life" typeId="percentage">
|
||||
<label>Battery Life</label>
|
||||
<description>Remaining Air Dial Battery Level</description>
|
||||
</channel>
|
||||
<channel id="filter_life" typeId="percentage">
|
||||
<label>Remaining Filter Life</label>
|
||||
<description>Remaining life of filter until exchange is necessary</description>
|
||||
</channel>
|
||||
</channels>
|
||||
</channel-group-type>
|
||||
|
||||
<!--Channel Definitions -->
|
||||
<channel-type id="currentTime">
|
||||
<item-type>DateTime</item-type>
|
||||
<label>Current Time</label>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
<channel-type id="mode">
|
||||
<item-type>String</item-type>
|
||||
<label>Mode</label>
|
||||
<description>Off, Demand, Manual, Program</description>
|
||||
<state>
|
||||
<options>
|
||||
<option value="DEMAND">Demand</option>
|
||||
<option value="PROGRAM">Program</option>
|
||||
<option value="MANUAL">Manual</option>
|
||||
<option value="OFF">Off</option>
|
||||
</options>
|
||||
</state>
|
||||
</channel-type>
|
||||
<channel-type id="manualFanSpeed">
|
||||
<item-type>Dimmer</item-type>
|
||||
<label>Manual Fan Speed</label>
|
||||
<state step="10" min="0" max="100"/>
|
||||
</channel-type>
|
||||
<channel-type id="supplyFanSpeed">
|
||||
<item-type>Number</item-type>
|
||||
<label>Supply Fan Speed</label>
|
||||
<state pattern="%.0f rpm" readOnly="true" min="0"/>
|
||||
</channel-type>
|
||||
<channel-type id="extractFanSpeed">
|
||||
<item-type>Number</item-type>
|
||||
<label>Extract Fan Speed</label>
|
||||
<state pattern="%.0f rpm" readOnly="true" min="0"/>
|
||||
</channel-type>
|
||||
<channel-type id="supplyFanStep">
|
||||
<item-type>Dimmer</item-type>
|
||||
<label>Supply Fan Step</label>
|
||||
<state step="10" min="0" max="100" readOnly="true"/>
|
||||
</channel-type>
|
||||
<channel-type id="extractFanStep">
|
||||
<item-type>Dimmer</item-type>
|
||||
<label>Extract Fan Step</label>
|
||||
<state step="10" min="0" max="100" readOnly="true"/>
|
||||
</channel-type>
|
||||
<channel-type id="percentage">
|
||||
<item-type>Number</item-type>
|
||||
<label>Percentage</label>
|
||||
<description>Read only percentage</description>
|
||||
<state pattern="%.0f %%" readOnly="true" min="0" max="100"/>
|
||||
</channel-type>
|
||||
<channel-type id="humidity">
|
||||
<item-type>Number</item-type>
|
||||
<label>Humidity</label>
|
||||
<category>Humidity</category>
|
||||
<tags>
|
||||
<tag>CurrentHumidity</tag>
|
||||
</tags>
|
||||
<state pattern="%.0f %%" readOnly="true" min="0" max="100">
|
||||
</state>
|
||||
</channel-type>
|
||||
<channel-type id="switch">
|
||||
<item-type>Switch</item-type>
|
||||
<label>Something that can be turned on or off</label>
|
||||
</channel-type>
|
||||
<channel-type id="temperature">
|
||||
<item-type>Number:Temperature</item-type>
|
||||
<label>Temperature</label>
|
||||
<description>Current temperature</description>
|
||||
<state readOnly="true" pattern="%.1f %unit%"/>
|
||||
</channel-type>
|
||||
|
||||
</thing:thing-descriptions>
|
||||
Reference in New Issue
Block a user