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,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>

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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
}

View File

@@ -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);
}
}

View File

@@ -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";
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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
}

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.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);
}
}

View File

@@ -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;
}
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -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>

View File

@@ -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>