[tradfri] Add support for Air Purifier (#14836)

* [tradfri] Added Support for Air Purifier (#7)

Added Support for Tradfri Air Purifier:
* Added documentation disambiguation Tradfri vs Dirigera
* Added Tradfri Air Purifier - fanMode and fanSpeed
* Workable Tradfri Air Purifier basic implementation
* Tradfri: modified fanMode type and definition
* Tradfri Air Purifier: Added disableLed
* Tradfri Air Purifier: Added lockPhysicalButton
* Tradfri Air Purifier: Added airQuality data
* Tradfri Air Purifier: Added filterCheck data
* Tradfri Air Purifier: Added translations
* Tradfri Air Purifier: Added filter_uptime
* Tradfri Air Purifier: code optimization
* Documentation for supported Air Purifier channels
Fixes #14816

Signed-off-by: Vivien Boistuaud <github@boistuaud.net>
This commit is contained in:
Vivien Boistuaud 2023-06-17 15:29:03 +02:00 committed by GitHub
parent b505f7b12c
commit 801b860c59
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 637 additions and 25 deletions

View File

@ -1,6 +1,7 @@
# TRÅDFRI Binding
This binding integrates the IKEA TRÅDFRI gateway and devices connected to it (such as dimmable LED bulbs).
This binding only supports IKEA TRÅDFRI gateway v1.x, it is **not** compatible with DIRIGERA.
## Supported Things
@ -22,22 +23,34 @@ These are:
| Non-Colour Controller | 0x0820 | 0820 |
| Non-Colour Scene Controller | 0x0830 | 0830 |
| Control Outlet | 0x0010 | 0010 |
| Window Covering Device | 0x0202 | 0202 |
| Window Covering Controller | 0x0202 | 0203 |
The following matrix lists the capabilities (channels) for each of the supported lighting device types:
| Thing type | Brightness | Color | Color Temperature | Battery Level | Battery Low | Power | Position |
|-------------|:----------:|:-----:|:-----------------:|:-------------:|:-----------:|:-----:|:---------|
| 0010 | | | | | | X | |
| 0100 | X | | | | | | |
| 0220 | X | | X | | | | |
| 0210 | | X | X | | | | |
| 0107 | | | | X | X | | |
| 0820 | | | | X | X | | |
| 0830 | | | | X | X | | |
| 0202 | | | | X | X | | X |
| 0203 | | | | X | X | | |
| Thing type | Brightness | Color | Color Temperature | Battery Level | Battery Low | Power |
|-------------|:----------:|:-----:|:-----------------:|:-------------:|:-----------:|:-----:|
| 0010 | | | | | | X |
| 0100 | X | | | | | |
| 0220 | X | | X | | | |
| 0210 | | X | X | | | |
| 0107 | | | | X | X | |
| 0820 | | | | X | X | |
| 0830 | | | | X | X | |
The following things are also supported even thought they are not standardized in Zigbee Light Link:
| Device type | Zigbee Device ID | Thing type |
|---------------------------------|------------------|------------|
| Window Covering Device | 0x0202 | 0202 |
| Window Covering Controller | 0x0203 | 0203 |
| Air Purifier | 0x0007 | 0007 |
The following matrix lists the capabilities (channels) for each of the supported non-lighting device types:
| Thing type | Battery Level | Battery Low | Position | Fan Mode | Lock Button | Disabled LED | Air Quality | Fan Speed | Filter Check | Filter Uptime |
|-------------|:-------------:|:-----------:|:--------:|:--------:|:-----------:|:------------:|:-----------:|:---------:|:------------:|:-------------:|
| 0202 | X | X | X | | | | | | | |
| 0203 | X | X | | | | | | | | |
| 0007 | | | | X | X | X | X | X | X | X |
## Thing Configuration
@ -65,17 +78,33 @@ The control outlet supports the `power` channel.
A blind or curtain supports, beside `battery_level` and `battery_low` channels, a `positon` channel.
Refer to the matrix above.
An air purifier supports:
* `fan_mode` and `fan_speed` channels, which allows for control of the fan and reading of the current speed.
* `disable_led` and `lock_button` channels, to respectively disable the LED's and lock the button on the physical device.
* `air_quality_pm25` and `air_quality_rating` channels, which reads the particulate matter 2.5μm and corresponding indication of air quality (similar to Tradfri app rating).
* `filter_check_next` and `filter_check_alarm` channels, which represents the remaining number of minutes until the next filter check and whether it is time to do the filter check now. Filter check must be completed through the TRÅDFRI app (or on the hardware buttons in case of replacement).
* a `filter_uptime` channel, which represents the current time since last filter change.
| Channel Type ID | Item Type | Description |
|-------------------|---------------|--------------------------------------------------------|
| brightness | Dimmer | The brightness of the bulb in percent |
| color_temperature | Dimmer | color temperature from 0% = cold to 100% = warm |
| color | Color | full color |
| battery_level | Number | battery level (in %) |
| battery_low | Switch | battery low warning (<=10% = ON, >10% = OFF) |
| power | Switch | power switch |
| position | Rollershutter | position of the blinds from 0% = open to 100% = closed |
Refer to the matrixes above.
| Channel Type ID | Item Type | Description |
|---------------------|----------------------|----------------------------------------------------------------------------------------------|
| brightness | Dimmer | The brightness of the bulb in percent |
| color_temperature | Dimmer | Color temperature from 0% = cold to 100% = warm |
| color | Color | Full color |
| battery_level | Number | Battery level (in %) |
| battery_low | Switch | Battery low warning (<=10% = ON, >10% = OFF) |
| power | Switch | Power switch |
| position | Rollershutter | Position of the blinds from 0% = open to 100% = closed |
| fan_mode | Number | Fan mode, target speed of the fan (0 = off, 1 = auto, 10..50 = Level 1 to 5) |
| fan_speed | Number | Current Fan Speed between 0 (off) and 50 (maximum speed) |
| disable_led | Switch | Disables the LED's on the device |
| lock_button | Switch | Disables the physical button on the device (applications can still make changes) |
| air_quality_pm25 | Number:Dimensionless | Density of Particulate Matter of 2.5μm, measured in ppm |
| air_quality_rating | Number | Gives a rating about air quality (1 = Good, 2 = OK, 3 = Bad) similar to Tradfri app |
| filter_check_next | Number:Time | Time in minutes before the next filter check if > 0, if < 0 you are late checking the filter |
| filter_check_alarm | Switch | When ON, you must perform a filter check (i.e. `filter_check_next` is < 0) |
| filter_uptime | Number:Time | Time elapsed since the last filter change, in minutes |
## Full Example
@ -89,6 +118,7 @@ Bridge tradfri:gateway:mygateway [ host="192.168.0.177", code="EHPW5rIJKyXFgjH3"
0830 myRemoteControl "My Remote Control" [ id=65545 ]
0010 myControlOutlet "My Control Outlet" [ id=65542 ]
0202 myBlinds "My Blinds" [ id=65547 ]
0007 myAirPurifier "My Air Purifier" [ id=65548 ]
}
```
@ -103,6 +133,15 @@ Number RemoteControlBatteryLevel { channel="tradfri:0830:mygateway:myRemoteContr
Switch RemoteControlBatteryLow { channel="tradfri:0830:mygateway:myRemoteControl:battery_low" }
Switch ControlOutlet { channel="tradfri:0010:mygateway:myControlOutlet:power" }
Rollershutter BlindPosition { channel="tradfri:0202:mygateway:myBlinds:position" }
Number AirPurifierFanMode { channel="tradfri:0007:mygateway:myAirPurifier:fan_mode" }
Number AirPurifierFanSpeed { channel="tradfri:0007:mygateway:myAirPurifier:fan_speed" }
Switch AirPurifierDisableLED { channel="tradfri:0007:mygateway:myAirPurifier:disable_led" }
Switch AirPurifierLockPhysicalButton { channel="tradfri:0007:mygateway:myAirPurifier:lock_button" }
Number AirPurifierQualityPM25 { channel="tradfri:0007:mygateway:myAirPurifier:air_quality_pm25" }
Number AirPurifierQualityRating { channel="tradfri:0007:mygateway:myAirPurifier:air_quality_rating" }
Number AirPurifierFilterCheckTTL { channel="tradfri:0007:mygateway:myAirPurifier:filter_check_next" }
Switch AirPurifierFilterCheckAlarm { channel="tradfri:0007:mygateway:myAirPurifier:filter_check_alarm" }
Number AirPurifierFilterUptime { channel="tradfri:0007:mygateway:myAirPurifier:filter_uptime" }
```
demo.sitemap:
@ -119,6 +158,15 @@ sitemap demo label="Main Menu"
Switch item=RemoteControlBatteryLow label="Battery Low Warning"
Switch item=ControlOutlet label="Power Switch"
Switch item=BlindPosition label="Blind Position [%d]"
Selection item=AirPurifierFanMode label="Fan Mode"
Text item=AirPurifierFanSpeed label="Current Fan Speed [%d]"
Switch item=AirPurifierDisableLED label="Disable LEDs"
Switch item=AirPurifierLockPhysicalButton label="Disable Physical Buttons"
Text item=AirPurifierQualityPM25 label="PM2.5"
Text item=AirPurifierQualityRating label="Air Quality"
Text item=AirPurifierFilterCheckTTL label="TTL before next filter check [%d min]"
Text item=AirPurifierFilterCheckAlarm label="Need to Check Filter [%s]"
Text item=AirPurifierFilterUptime label="Current filter uptime [%d min]"
}
}
```

View File

@ -27,6 +27,7 @@ import org.openhab.core.thing.ThingTypeUID;
* @author Kai Kreuzer - Initial contribution
* @author Christoph Weitkamp - Added support for remote controller and motion sensor devices (read-only battery level)
* @author Manuel Raffel - Added support for blinds
* @author Vivien Boistuaud - Added support for air purifier
*/
@NonNullByDefault
public class TradfriBindingConstants {
@ -45,6 +46,7 @@ public class TradfriBindingConstants {
public static final ThingTypeUID THING_TYPE_MOTION_SENSOR = new ThingTypeUID(BINDING_ID, "0107");
public static final ThingTypeUID THING_TYPE_BLINDS = new ThingTypeUID(BINDING_ID, "0202");
public static final ThingTypeUID THING_TYPE_OPEN_CLOSE_REMOTE_CONTROL = new ThingTypeUID(BINDING_ID, "0203");
public static final ThingTypeUID THING_TYPE_AIR_PURIFIER = new ThingTypeUID(BINDING_ID, "0007");
public static final Set<ThingTypeUID> SUPPORTED_LIGHT_TYPES_UIDS = Collections
.unmodifiableSet(Stream.of(THING_TYPE_DIMMABLE_LIGHT, THING_TYPE_COLOR_TEMP_LIGHT, THING_TYPE_COLOR_LIGHT)
@ -54,6 +56,8 @@ public class TradfriBindingConstants {
public static final Set<ThingTypeUID> SUPPORTED_BLINDS_TYPES_UIDS = Collections.singleton(THING_TYPE_BLINDS);
public static final Set<ThingTypeUID> SUPPORTED_AIR_PURIFIER_TYPES_UIDS = Set.of(THING_TYPE_AIR_PURIFIER);
// List of all Gateway Configuration Properties
public static final String GATEWAY_CONFIG_HOST = "host";
public static final String GATEWAY_CONFIG_PORT = "port";
@ -70,7 +74,8 @@ public class TradfriBindingConstants {
public static final Set<ThingTypeUID> SUPPORTED_DEVICE_TYPES_UIDS = Collections.unmodifiableSet(Stream
.of(SUPPORTED_LIGHT_TYPES_UIDS.stream(), SUPPORTED_CONTROLLER_TYPES_UIDS.stream(),
SUPPORTED_PLUG_TYPES_UIDS.stream(), SUPPORTED_BLINDS_TYPES_UIDS.stream())
SUPPORTED_PLUG_TYPES_UIDS.stream(), SUPPORTED_BLINDS_TYPES_UIDS.stream(),
SUPPORTED_AIR_PURIFIER_TYPES_UIDS.stream())
.reduce(Stream::concat).orElseGet(Stream::empty).collect(Collectors.toSet()));
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections
@ -85,9 +90,20 @@ public class TradfriBindingConstants {
public static final String CHANNEL_POSITION = "position";
public static final String CHANNEL_BATTERY_LEVEL = "battery_level";
public static final String CHANNEL_BATTERY_LOW = "battery_low";
public static final String CHANNEL_FAN_MODE = "fan_mode";
public static final String CHANNEL_FAN_SPEED = "fan_speed";
public static final String CHANNEL_DISABLE_LED = "disable_led";
public static final String CHANNEL_LOCK_BUTTON = "lock_button";
public static final String CHANNEL_AIR_QUALITY_PM25 = "air_quality_pm25";
public static final String CHANNEL_AIR_QUALITY_RATING = "air_quality_rating";
public static final String CHANNEL_FILTER_CHECK_NEXT = "filter_check_next";
public static final String CHANNEL_FILTER_CHECK_ALARM = "filter_check_alarm";
public static final String CHANNEL_FILTER_UPTIME = "filter_uptime";
// IPSO Objects
public static final String DEVICES = "15001";
public static final String AIR_PURIFIER = "15025";
public static final String AIR_QUALITY = "5907";
public static final String AUTH_PATH = "9063";
public static final String BLINDS = "15015";
public static final String CLIENT_IDENTITY_PROPOSED = "9090";
@ -107,6 +123,9 @@ public class TradfriBindingConstants {
public static final String END_TIME_HR = "9048";
public static final String END_TIME_MN = "9049";
public static final String ERROR_TAG = "errorcode";
public static final String FAN_MODE = "5900";
public static final String FAN_SPEED = "5908";
public static final String FILTER_UPTIME = "5902";
public static final String FORCE_CHECK_OTA_UPDATE = "9032";
public static final String GATEWAY = "15011";
public static final String GATEWAY_DETAILS = "15012";
@ -125,9 +144,11 @@ public class TradfriBindingConstants {
public static final String IKEA_MOODS = "9068";
public static final String INSTANCE_ID = "9003";
public static final String LAST_SEEN = "9020";
public static final String LED_DISABLE = "5906";
public static final String LIGHT = "3311";
public static final int LIGHTS_OFF_SMART_TASK = 2;
public static final String LIGHT_SETTING = "15013";
public static final String LOCK_PHYSICAL_BUTTON = "5905";
public static final int LOSS_OF_INTERNET_CONNECTIVITY = 5001;
public static final String MASTER_TOKEN_TAG = "9036";
public static final String MAX_MSR_VALUE = "5602";
@ -137,6 +158,7 @@ public class TradfriBindingConstants {
public static final String NAME = "9001";
public static final int NEW_FIRMWARE_AVAILABLE = 1001;
public static final String NEW_PSK_BY_GW = "9091";
public static final String NEXT_FILTER_CHECK = "5910";
public static final String NOTIFICATION_EVENT = "9015";
public static final String NOTIFICATION_NVPAIR = "9017";
public static final String NOTIFICATION_STATE = "9014";
@ -201,8 +223,27 @@ public class TradfriBindingConstants {
public static final String TYPE_SENSOR = "4";
public static final String TYPE_REPEATER = "6";
public static final String TYPE_BLINDS = "7";
public static final String TYPE_AIR_PURIFIER = "10";
public static final String DEVICE_VENDOR = "0";
public static final String DEVICE_MODEL = "1";
public static final String DEVICE_FIRMWARE = "3";
public static final String DEVICE_BATTERY_LEVEL = "9";
// List of Air Purifier Constants
public static final int FAN_MODE_OFF = 0;
public static final int FAN_MODE_AUTO = 1;
public static final int FAN_MODE_SPEED1 = 10;
public static final int FAN_MODE_SPEED2 = 20;
public static final int FAN_MODE_SPEED3 = 30;
public static final int FAN_MODE_SPEED4 = 40;
public static final int FAN_MODE_SPEED5 = 50;
public static final Set<Integer> AIR_PURIFIER_FANMODE = Set.of(FAN_MODE_OFF, FAN_MODE_AUTO, FAN_MODE_SPEED1,
FAN_MODE_SPEED2, FAN_MODE_SPEED3, FAN_MODE_SPEED4, FAN_MODE_SPEED5);
public static final int AIR_PURIFIER_AIR_QUALITY_OK = 36;
public static final int AIR_PURIFIER_AIR_QUALITY_BAD = 86;
public static final int AIR_PURIFIER_AIR_QUALITY_UNDEFINED = 65535;
}

View File

@ -16,6 +16,7 @@ import static org.openhab.binding.tradfri.internal.TradfriBindingConstants.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.tradfri.internal.handler.TradfriAirPurifierHandler;
import org.openhab.binding.tradfri.internal.handler.TradfriBlindHandler;
import org.openhab.binding.tradfri.internal.handler.TradfriControllerHandler;
import org.openhab.binding.tradfri.internal.handler.TradfriGatewayHandler;
@ -63,6 +64,8 @@ public class TradfriHandlerFactory extends BaseThingHandlerFactory {
return new TradfriLightHandler(thing);
} else if (SUPPORTED_PLUG_TYPES_UIDS.contains(thingTypeUID)) {
return new TradfriPlugHandler(thing);
} else if (THING_TYPE_AIR_PURIFIER.equals(thingTypeUID)) {
return new TradfriAirPurifierHandler(thing);
}
return null;
}

View File

@ -49,6 +49,7 @@ import com.google.gson.JsonSyntaxException;
* @author Christoph Weitkamp - Added support for remote controller and motion sensor devices (read-only battery level)
* @author Andre Fuechsel - fixed the results removal
* @author Manuel Raffel - Added support for blinds
* @author Vivien Boistuaud - Added support for Air Purifiers
*/
@NonNullByDefault
public class TradfriDiscoveryService extends AbstractDiscoveryService
@ -158,6 +159,8 @@ public class TradfriDiscoveryService extends AbstractDiscoveryService
} else if (TYPE_SENSOR.equals(type) && data.has(SENSOR)) {
// Motion sensor
thingId = new ThingUID(THING_TYPE_MOTION_SENSOR, bridge, Integer.toString(id));
} else if (TYPE_AIR_PURIFIER.equals(type) && data.has(AIR_PURIFIER)) {
thingId = new ThingUID(THING_TYPE_AIR_PURIFIER, bridge, Integer.toString(id));
}
if (thingId == null) {

View File

@ -0,0 +1,163 @@
/**
* Copyright (c) 2010-2023 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.tradfri.internal.handler;
import static org.openhab.binding.tradfri.internal.TradfriBindingConstants.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.tradfri.internal.TradfriCoapClient;
import org.openhab.binding.tradfri.internal.model.TradfriAirPurifierData;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.State;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.JsonElement;
/**
* The {@link TradfriAirPurifierHandler} is responsible for handling commands and status updates
* for Starkvind Air Purifiers.
*
* @author Vivien Boistuaud - Initial contribution
*/
@NonNullByDefault
public class TradfriAirPurifierHandler extends TradfriThingHandler {
private final Logger logger = LoggerFactory.getLogger(TradfriAirPurifierHandler.class);
public TradfriAirPurifierHandler(Thing thing) {
super(thing);
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (active) {
if (command instanceof RefreshType) {
TradfriCoapClient coapClient = this.coapClient;
if (coapClient != null) {
logger.debug("Refreshing channel {}", channelUID);
coapClient.asyncGet(this);
} else {
logger.debug("coapClient is null!");
}
return;
}
switch (channelUID.getId()) {
case CHANNEL_FAN_MODE:
handleFanModeCommand(command);
break;
case CHANNEL_DISABLE_LED:
handleDisableLed(command);
break;
case CHANNEL_LOCK_BUTTON:
handleLockButton(command);
break;
default:
logger.error("Unknown channel UID {}", channelUID);
}
}
}
private void handleFanModeCommand(Command command) {
if (command instanceof Number) {
set(new TradfriAirPurifierData().setFanMode((Number) command).getJsonString());
} else {
logger.debug("Cannot handle command '{}' of type {} for channel '{}'", command, command.getClass(),
CHANNEL_FAN_MODE);
}
}
private void handleDisableLed(Command command) {
if (command instanceof OnOffType) {
set(new TradfriAirPurifierData().setDisableLed((OnOffType) command).getJsonString());
} else {
logger.debug("Cannot handle command '{}' of type {} for channel '{}'", command, command.getClass(),
CHANNEL_DISABLE_LED);
}
}
private void handleLockButton(Command command) {
if (command instanceof OnOffType) {
set(new TradfriAirPurifierData().setLockPhysicalButton((OnOffType) command).getJsonString());
} else {
logger.debug("Cannot handle command '{}' of type {} for channel '{}'", command, command.getClass(),
CHANNEL_DISABLE_LED);
}
}
@Override
public void onUpdate(JsonElement data) {
if (active && !(data.isJsonNull())) {
TradfriAirPurifierData state = new TradfriAirPurifierData(data);
updateStatus(state.getReachabilityStatus() ? ThingStatus.ONLINE : ThingStatus.OFFLINE);
State fanMode = state.getFanMode();
if (fanMode != null) {
updateState(CHANNEL_FAN_MODE, fanMode);
}
State fanSpeed = state.getFanSpeed();
if (fanSpeed != null) {
updateState(CHANNEL_FAN_SPEED, fanSpeed);
}
State disableLed = state.getDisableLed();
if (disableLed != null) {
updateState(CHANNEL_DISABLE_LED, disableLed);
}
State lockPhysicalButton = state.getLockPhysicalButton();
if (lockPhysicalButton != null) {
updateState(CHANNEL_LOCK_BUTTON, lockPhysicalButton);
}
State airQualityPm25 = state.getAirQualityPM25();
if (airQualityPm25 != null) {
updateState(CHANNEL_AIR_QUALITY_PM25, airQualityPm25);
}
State airQualityRating = state.getAirQualityRating();
if (airQualityRating != null) {
updateState(CHANNEL_AIR_QUALITY_RATING, airQualityRating);
}
State nextFilterCheckTTL = state.getNextFilterCheckTTL();
if (nextFilterCheckTTL != null) {
updateState(CHANNEL_FILTER_CHECK_NEXT, nextFilterCheckTTL);
}
State filterCheckAlarm = state.getFilterCheckAlarm();
if (filterCheckAlarm != null) {
updateState(CHANNEL_FILTER_CHECK_ALARM, filterCheckAlarm);
}
State filterUptime = state.getFilterUptime();
if (filterUptime != null) {
updateState(CHANNEL_FILTER_UPTIME, filterUptime);
}
logger.debug(
"Updating thing for airPurifierId {} to state {fanMode: {}, fanSpeed: {}, disableLed: {}, lockButton: {}, airQualityPm25: {}, airQualityRating: {}, nextFilterCheckTTL: {}, filterCheckAlarm: {}, filterUptime: {}, firmwareVersion: {}, modelId: {}, vendor: {}}",
state.getDeviceId(), state.getFanMode(), state.getFanSpeed(), state.getDisableLed(),
state.getLockPhysicalButton(), state.getAirQualityPM25(), state.getAirQualityRating(),
state.getNextFilterCheckTTL(), state.getFilterCheckAlarm(), state.getFilterUptime(),
state.getFirmwareVersion(), state.getModelId(), state.getVendor());
}
}
}

View File

@ -0,0 +1,185 @@
/**
* Copyright (c) 2010-2023 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.tradfri.internal.model;
import static org.openhab.binding.tradfri.internal.TradfriBindingConstants.*;
import javax.measure.quantity.Dimensionless;
import javax.measure.quantity.Time;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.unit.Units;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.JsonElement;
import com.google.gson.JsonPrimitive;
/**
* The {@link TradfriAirPurifierData} class is a Java wrapper for the raw JSON data about the air purifier state.
*
* @author Vivien Boistuaud - Initial contribution
*/
@NonNullByDefault
public class TradfriAirPurifierData extends TradfriDeviceData {
private final Logger logger = LoggerFactory.getLogger(TradfriAirPurifierData.class);
public TradfriAirPurifierData() {
super(AIR_PURIFIER);
}
public TradfriAirPurifierData(JsonElement json) {
super(AIR_PURIFIER, json);
}
public String getJsonString() {
return root.toString();
}
public @Nullable DecimalType getFanMode() {
JsonElement fanMode = attributes.get(FAN_MODE);
if (fanMode != null) {
int modeValue = fanMode.getAsInt();
if (AIR_PURIFIER_FANMODE.contains(modeValue)) {
return new DecimalType(modeValue);
} else {
logger.debug("Invalid speedMode is '{}': unknown value", modeValue);
return null;
}
} else {
return null;
}
}
public TradfriAirPurifierData setFanMode(Number speedValue) {
int speed = speedValue.intValue();
if (AIR_PURIFIER_FANMODE.contains(speed)) {
attributes.add(FAN_MODE, new JsonPrimitive(speed));
} else {
logger.debug("Could not set fanMode to '{}': unknown value", speed);
}
return this;
}
public @Nullable DecimalType getFanSpeed() {
JsonElement fanSpeed = attributes.get(FAN_SPEED);
if (fanSpeed != null) {
int speedValue = fanSpeed.getAsInt();
return new DecimalType(speedValue);
} else {
return null;
}
}
public @Nullable OnOffType getDisableLed() {
JsonElement ledOnOff = attributes.get(LED_DISABLE);
if (ledOnOff != null) {
boolean ledStatus = ledOnOff.getAsInt() != 0;
return OnOffType.from(ledStatus);
} else {
return null;
}
}
public TradfriAirPurifierData setDisableLed(OnOffType disableOnOff) {
attributes.add(LED_DISABLE, new JsonPrimitive(OnOffType.ON.equals(disableOnOff) ? 1 : 0));
return this;
}
public @Nullable OnOffType getLockPhysicalButton() {
JsonElement lockPhysicalButton = attributes.get(LOCK_PHYSICAL_BUTTON);
if (lockPhysicalButton != null) {
boolean isLocked = lockPhysicalButton.getAsInt() != 0;
return OnOffType.from(isLocked);
} else {
return null;
}
}
public TradfriAirPurifierData setLockPhysicalButton(OnOffType lockPhysicalButton) {
attributes.add(LOCK_PHYSICAL_BUTTON, new JsonPrimitive(OnOffType.ON.equals(lockPhysicalButton) ? 1 : 0));
return this;
}
public @Nullable State getAirQualityPM25() {
JsonElement airQuality = attributes.get(AIR_QUALITY);
if (airQuality != null) {
int pm25InPpm = airQuality.getAsInt();
if (pm25InPpm != AIR_PURIFIER_AIR_QUALITY_UNDEFINED) {
return new QuantityType<Dimensionless>(pm25InPpm, Units.PARTS_PER_MILLION);
} else {
return UnDefType.UNDEF;
}
} else {
return null;
}
}
public @Nullable State getAirQualityRating() {
State pm25State = getAirQualityPM25();
if (pm25State != null) {
if (pm25State instanceof Number) {
int pm25Value = ((Number) pm25State).intValue();
int qualityRating = 1;
if (pm25Value >= AIR_PURIFIER_AIR_QUALITY_BAD) {
qualityRating = 3;
} else if (pm25Value >= AIR_PURIFIER_AIR_QUALITY_OK) {
qualityRating = 2;
}
return new DecimalType(qualityRating);
}
return UnDefType.UNDEF;
} else {
return null;
}
}
public @Nullable QuantityType<Time> getNextFilterCheckTTL() {
JsonElement nextFilterCheckTTL = attributes.get(NEXT_FILTER_CHECK);
if (nextFilterCheckTTL != null) {
int remainingMinutes = nextFilterCheckTTL.getAsInt();
return new QuantityType<Time>(remainingMinutes, Units.MINUTE);
} else {
return null;
}
}
public @Nullable OnOffType getFilterCheckAlarm() {
QuantityType<Time> ttl = getNextFilterCheckTTL();
if (ttl != null) {
int ttlValue = ttl.intValue();
return ttlValue < 0 ? OnOffType.ON : OnOffType.OFF;
} else {
return null;
}
}
public @Nullable QuantityType<Time> getFilterUptime() {
JsonElement filterUptime = attributes.get(FILTER_UPTIME);
if (filterUptime != null) {
int filterUptimeMinutes = filterUptime.getAsInt();
return new QuantityType<Time>(filterUptimeMinutes, Units.MINUTE);
} else {
return null;
}
}
}

View File

@ -5,6 +5,8 @@ addon.tradfri.description = This binding supports IKEA TRÅDFRI lighting devices
# thing types
thing-type.tradfri.0007.label = Air Purifier
thing-type.tradfri.0007.description = This represents the air purifier sensors and controls.
thing-type.tradfri.0010.label = On/Off Plug
thing-type.tradfri.0010.description = A plug that can be switched on and off.
thing-type.tradfri.0100.label = Dimmable Light
@ -43,6 +45,34 @@ thing-type.config.tradfri.device.id.description = The identifier of the device o
# channel types
channel-type.tradfri.air-quality-pm25.label = PM 2.5
channel-type.tradfri.air-quality-pm25.description = Density of Particulate Matter of 2.5μm measured by the Air Purifier, in ppm.
channel-type.tradfri.air-quality-rating.label = Air Quality
channel-type.tradfri.air-quality-rating.description = An evaluation of the Air Quality between 1 (Good) and 3 (Bad).
channel-type.tradfri.air-quality-rating.state.option.1 = Good
channel-type.tradfri.air-quality-rating.state.option.2 = OK
channel-type.tradfri.air-quality-rating.state.option.3 = Bad
channel-type.tradfri.disable-led.label = Disable LED
channel-type.tradfri.disable-led.description = Disables the LED's on the Air Purifier.
channel-type.tradfri.fan-mode.label = Fan Speed Mode
channel-type.tradfri.fan-mode.description = Controls the configured fan speed.
channel-type.tradfri.fan-mode.state.option.0 = Power Off
channel-type.tradfri.fan-mode.state.option.1 = Automatic
channel-type.tradfri.fan-mode.state.option.10 = Speed 1
channel-type.tradfri.fan-mode.state.option.20 = Speed 2
channel-type.tradfri.fan-mode.state.option.30 = Speed 3
channel-type.tradfri.fan-mode.state.option.40 = Speed 4
channel-type.tradfri.fan-mode.state.option.50 = Speed 5
channel-type.tradfri.fan-speed.label = Current Fan Speed
channel-type.tradfri.fan-speed.description = Displays the current fan speed (0..50).
channel-type.tradfri.filter-check-alarm.label = Filter Check Alarm
channel-type.tradfri.filter-check-alarm.description = When this value is ON, you need to perform a filter check.
channel-type.tradfri.filter-check-next.label = Next Filter Check
channel-type.tradfri.filter-check-next.description = The number of minutes before the next filter check (or time since check is required if below 0).
channel-type.tradfri.filter-uptime.label = Filter Uptime
channel-type.tradfri.filter-uptime.description = The duration for which the current filter was used.
channel-type.tradfri.lock-button.label = Lock Physical Button
channel-type.tradfri.lock-button.description = When ON, locks the physical button on the device.
channel-type.tradfri.position.label = Position
channel-type.tradfri.position.description = Control the position of the blind or curtain in percent from 0 (open) to 100 (closed).

View File

@ -176,6 +176,31 @@
<config-description-ref uri="thing-type:tradfri:device"/>
</thing-type>
<thing-type id="0007">
<supported-bridge-type-refs>
<bridge-type-ref id="gateway"/>
</supported-bridge-type-refs>
<label>Air Purifier</label>
<description>This represents the air purifier sensors and controls.</description>
<channels>
<channel id="fan_speed" typeId="fan-speed"/>
<channel id="fan_mode" typeId="fan-mode"/>
<channel id="disable_led" typeId="disable-led"/>
<channel id="lock_button" typeId="lock-button"/>
<channel id="air_quality_pm25" typeId="air-quality-pm25"/>
<channel id="air_quality_rating" typeId="air-quality-rating"/>
<channel id="filter_check_next" typeId="filter-check-next"/>
<channel id="filter_check_alarm" typeId="filter-check-alarm"/>
<channel id="filter_uptime" typeId="filter-uptime"/>
</channels>
<representation-property>id</representation-property>
<config-description-ref uri="thing-type:tradfri:device"/>
</thing-type>
<channel-type id="position">
<item-type>Rollershutter</item-type>
<label>Position</label>
@ -183,4 +208,100 @@
<category>Blinds</category>
</channel-type>
<channel-type id="fan-mode">
<item-type>Number:Dimensionless</item-type>
<label>Fan Speed Mode</label>
<description>Controls the configured fan speed.</description>
<state pattern="%d">
<options>
<option value="0">Power Off</option>
<option value="1">Automatic</option>
<option value="10">Speed 1</option>
<option value="20">Speed 2</option>
<option value="30">Speed 3</option>
<option value="40">Speed 4</option>
<option value="50">Speed 5</option>
</options>
</state>
</channel-type>
<channel-type id="fan-speed">
<item-type>Number:Dimensionless</item-type>
<label>Current Fan Speed</label>
<description>Displays the current fan speed (0..50).</description>
<category>Fan</category>
<state readOnly="true"/>
</channel-type>
<channel-type id="disable-led">
<item-type>Switch</item-type>
<label>Disable LED</label>
<description>Disables the LED's on the Air Purifier.</description>
</channel-type>
<channel-type id="lock-button">
<item-type>Switch</item-type>
<label>Lock Physical Button</label>
<description>When ON, locks the physical button on the device.</description>
</channel-type>
<channel-type id="air-quality-pm25">
<item-type>Number:Dimensionless</item-type>
<label>PM 2.5</label>
<description>Density of Particulate Matter of 2.5μm measured by the Air Purifier, in ppm.</description>
<state readOnly="true" pattern="%d ppm"/>
</channel-type>
<channel-type id="air-quality-rating">
<item-type>Number:Dimensionless</item-type>
<label>Air Quality</label>
<description>An evaluation of the Air Quality between 1 (Good) and 3 (Bad).</description>
<state readOnly="true" pattern="%d">
<options>
<option value="1">Good</option>
<option value="2">OK</option>
<option value="3">Bad</option>
</options>
</state>
</channel-type>
<channel-type id="filter-check-next">
<item-type>Number:Time</item-type>
<label>Next Filter Check</label>
<description>The number of minutes before the next filter check (or time since check is required if below 0).</description>
<category>Time</category>
<state readOnly="true" pattern="%.2f d"/>
</channel-type>
<channel-type id="filter-check-alarm">
<item-type>Switch</item-type>
<label>Filter Check Alarm</label>
<description>When this value is ON, you need to perform a filter check.</description>
<state readOnly="true" pattern="%s"/>
</channel-type>
<channel-type id="filter-uptime">
<item-type>Number:Time</item-type>
<label>Filter Uptime</label>
<description>The duration for which the current filter was used.</description>
<category>Time</category>
<state readOnly="true" pattern="%.2f week"/>
</channel-type>
</thing:thing-descriptions>

View File

@ -46,6 +46,7 @@ import com.google.gson.JsonParser;
*
* @author Kai Kreuzer - Initial contribution
* @author Christoph Weitkamp - Added support for remote controller and motion sensor devices (read-only battery level)
* @author Vivien Boistuaud - Added unit test for Air Purifier
*/
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
@ -92,7 +93,7 @@ public class TradfriDiscoveryServiceTest {
@Test
public void correctSupportedTypes() {
assertThat(discovery.getSupportedThingTypes().size(), is(9));
assertThat(discovery.getSupportedThingTypes().size(), is(10));
assertTrue(discovery.getSupportedThingTypes().contains(THING_TYPE_DIMMABLE_LIGHT));
assertTrue(discovery.getSupportedThingTypes().contains(THING_TYPE_COLOR_TEMP_LIGHT));
assertTrue(discovery.getSupportedThingTypes().contains(THING_TYPE_COLOR_LIGHT));
@ -102,6 +103,7 @@ public class TradfriDiscoveryServiceTest {
assertTrue(discovery.getSupportedThingTypes().contains(THING_TYPE_OPEN_CLOSE_REMOTE_CONTROL));
assertTrue(discovery.getSupportedThingTypes().contains(THING_TYPE_ONOFF_PLUG));
assertTrue(discovery.getSupportedThingTypes().contains(THING_TYPE_BLINDS));
assertTrue(discovery.getSupportedThingTypes().contains(THING_TYPE_AIR_PURIFIER));
}
@Test
@ -233,4 +235,20 @@ public class TradfriDiscoveryServiceTest {
assertThat(discoveryResult.getProperties().get(CONFIG_ID), is(65538));
assertThat(discoveryResult.getRepresentationProperty(), is(CONFIG_ID));
}
@Test
public void validDiscoveryResultAirPurifier() {
String json = "{\"3\":{\"0\":\"IKEAofSweden\",\"1\":\"STARKVINDAirpurifier\",\"2\":\"\",\"3\":\"1.0.033\",\"6\":1,\"7\":4364},\"5750\":10,\"9001\":\"Luftreiniger\",\"9002\":1633096623,\"9003\":65548,\"9019\":1,\"9020\":1633096633,\"9054\":0,\"15025\":[{\"5900\":1,\"5902\":2,\"5903\":0,\"5904\":259200,\"5905\":0,\"5906\":0,\"5907\":5,\"5908\":10,\"5909\":2,\"5910\":259198,\"9003\":0}]}";
JsonObject data = JsonParser.parseString(json).getAsJsonObject();
discovery.onUpdate("65548", data);
assertNotNull(discoveryResult);
assertThat(discoveryResult.getFlag(), is(DiscoveryResultFlag.NEW));
assertThat(discoveryResult.getThingUID(), is(new ThingUID("tradfri:0007:1:65548")));
assertThat(discoveryResult.getThingTypeUID(), is(THING_TYPE_AIR_PURIFIER));
assertThat(discoveryResult.getBridgeUID(), is(GATEWAY_THING_UID));
assertThat(discoveryResult.getProperties().get(CONFIG_ID), is(65548));
assertThat(discoveryResult.getRepresentationProperty(), is(CONFIG_ID));
}
}