[deconz] Support for air quality sensor (#11885)

[deconz] Support for air quality sensor

Signed-off-by: Philipp Schneider <philipp.schneider@nixo-soft.de>
This commit is contained in:
Philipp S 2022-01-06 09:44:59 +01:00 committed by GitHub
parent b9ad9ae29e
commit 5dc26419d1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 143 additions and 6 deletions

View File

@ -26,6 +26,7 @@ These sensors are supported:
| Vibration Sensor | ZHAVibration | `vibrationsensor` |
| deCONZ Artificial Daylight Sensor | deCONZ specific: simulated sensor | `daylightsensor` |
| Carbon-Monoxide Sensor | ZHACarbonmonoxide | `carbonmonoxide` |
| Air quality Sensor | ZHAAirQuality | `airqualitysensor` |
| Color Controller | ZBT-Remote-ALL-RGBW | `colorcontrol` |
@ -150,6 +151,8 @@ The sensor devices support some of the following channels:
| battery_level | Number | R | Battery level (in %) | any battery-powered sensor |
| battery_low | Switch | R | Battery level low: `ON`; `OFF` | any battery-powered sensor |
| carbonmonoxide | Switch | R | `ON` = carbon monoxide detected | carbonmonoxide |
| airquality | String | R | Current air quality level | airqualitysensor |
| airqualityppb | Number:Dimensionless | R | Current air quality ppb (parts per billion) | airqualitysensor |
| color | Color | R | Color set by remote | colorcontrol |
| windowopen | Contact | R | `windowopen` status is reported by some thermostats | thermostat |
@ -218,6 +221,7 @@ Bridge deconz:deconz:homeserver [ host="192.168.0.10", apikey="ABCDEFGHIJ" ] {
presencesensor livingroom-presence "Livingroom Presence" [ id="1" ]
temperaturesensor livingroom-temperature "Livingroom Temperature" [ id="2" ]
humiditysensor livingroom-humidity "Livingroom Humidity" [ id="3" ]
airqualitysensor livingroom-voc "Livingroom Voc" [ id="9" ]
pressuresensor livingroom-pressure "Livingroom Pressure" [ id="4" ]
openclosesensor livingroom-window "Livingroom Window" [ id="5" ]
switch livingroom-hue-tap "Livingroom Hue Tap" [ id="6" ]
@ -235,6 +239,8 @@ Bridge deconz:deconz:homeserver [ host="192.168.0.10", apikey="ABCDEFGHIJ" ] {
Switch Livingroom_Presence "Presence Livingroom [%s]" <motion> { channel="deconz:presencesensor:homeserver:livingroom-presence:presence" }
Number:Temperature Livingroom_Temperature "Temperature Livingroom [%.1f °C]" <temperature> { channel="deconz:temperaturesensor:homeserver:livingroom-temperature:temperature" }
Number:Dimensionless Livingroom_Humidity "Humidity Livingroom [%.1f %%]" <humidity> { channel="deconz:humiditysensor:homeserver:livingroom-humidity:humidity" }
String Livingroom_voc_label "Air quality Livingroom [%s]" { channel="deconz:airqualitysensor:homeserver:livingroom-voc:airquality" }
Number:Dimensionless Livingroom_voc "Air quality [%d ppb]" { channel="deconz:airqualitysensor:homeserver:livingroom-voc:airqualityppb" }
Number:Pressure Livingroom_Pressure "Pressure Livingroom [%.1f hPa]" <pressure> { channel="deconz:pressuresensor:homeserver:livingroom-pressure:pressure" }
Contact Livingroom_Window "Window Livingroom [%s]" <door> { channel="deconz:openclosesensor:homeserver:livingroom-window:open" }
Switch Basement_Water_Leakage "Basement Water Leakage [%s]" { channel="deconz:waterleakagesensor:homeserver:basement-water-leakage:waterleakage" }

View File

@ -49,6 +49,7 @@ public class BindingConstants {
public static final ThingTypeUID THING_TYPE_BATTERY_SENSOR = new ThingTypeUID(BINDING_ID, "batterysensor");
public static final ThingTypeUID THING_TYPE_CARBONMONOXIDE_SENSOR = new ThingTypeUID(BINDING_ID,
"carbonmonoxidesensor");
public static final ThingTypeUID THING_TYPE_AIRQUALITY_SENSOR = new ThingTypeUID(BINDING_ID, "airqualitysensor");
// Special sensor - Thermostat
public static final ThingTypeUID THING_TYPE_THERMOSTAT = new ThingTypeUID(BINDING_ID, "thermostat");
@ -98,6 +99,8 @@ public class BindingConstants {
public static final String CHANNEL_BATTERY_LEVEL = "battery_level";
public static final String CHANNEL_BATTERY_LOW = "battery_low";
public static final String CHANNEL_CARBONMONOXIDE = "carbonmonoxide";
public static final String CHANNEL_AIRQUALITY = "airquality";
public static final String CHANNEL_AIRQUALITYPPB = "airqualityppb";
public static final String CHANNEL_HEATSETPOINT = "heatsetpoint";
public static final String CHANNEL_THERMOSTAT_MODE = "mode";
public static final String CHANNEL_TEMPERATURE_OFFSET = "offset";

View File

@ -281,6 +281,8 @@ public class ThingDiscoveryService extends AbstractDiscoveryService implements D
thingTypeUID = THING_TYPE_BATTERY_SENSOR; // ZHABattery
} else if (sensor.type.contains("ZHAThermostat")) {
thingTypeUID = THING_TYPE_THERMOSTAT; // ZHAThermostat
} else if (sensor.type.contains("ZHAAirQuality")) {
thingTypeUID = THING_TYPE_AIRQUALITY_SENSOR;
} else {
logger.debug("Unknown type {}", sensor.type);
return;

View File

@ -54,6 +54,10 @@ public class SensorState {
public @Nullable Boolean vibration;
/** carbonmonoxide sensors provide a boolean value. */
public @Nullable Boolean carbonmonoxide;
/** airquality sensors provide a string value. */
public @Nullable String airquality;
/** airquality sensors provide a integer value. */
public @Nullable Integer airqualityppb;
/** Pressure sensors provide a hPa value. */
public @Nullable Integer pressure;
/** Presence sensors provide this boolean. */
@ -88,10 +92,11 @@ public class SensorState {
return "SensorState{" + "dark=" + dark + ", daylight=" + daylight + ", lightlevel=" + lightlevel + ", lux="
+ lux + ", temperature=" + temperature + ", humidity=" + humidity + ", open=" + open + ", fire=" + fire
+ ", water=" + water + ", alarm=" + alarm + ", tampered=" + tampered + ", vibration=" + vibration
+ ", carbonmonoxide=" + carbonmonoxide + ", pressure=" + pressure + ", presence=" + presence
+ ", power=" + power + ", battery=" + battery + ", consumption=" + consumption + ", voltage=" + voltage
+ ", current=" + current + ", status=" + status + ", buttonevent=" + buttonevent + ", gesture="
+ gesture + ", valve=" + valve + ", windowopen='" + windowopen + '\'' + ", lastupdated='" + lastupdated
+ '\'' + ", xy=" + Arrays.toString(xy) + '}';
+ ", carbonmonoxide=" + carbonmonoxide + ", airquality=" + airquality + ", airqualityppb="
+ airqualityppb + ", pressure=" + pressure + ", presence=" + presence + ", power=" + power
+ ", battery=" + battery + ", consumption=" + consumption + ", voltage=" + voltage + ", current="
+ current + ", status=" + status + ", buttonevent=" + buttonevent + ", gesture=" + gesture + ", valve="
+ valve + ", windowopen='" + windowopen + '\'' + ", lastupdated='" + lastupdated + '\'' + ", xy="
+ Arrays.toString(xy) + '}';
}
}

View File

@ -33,6 +33,7 @@ import org.openhab.binding.deconz.internal.types.ResourceType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
@ -250,6 +251,10 @@ public abstract class SensorBaseThingHandler extends DeconzBaseThingHandler {
updateState(channelUID, OnOffType.from(value));
}
protected void updateStringChannel(ChannelUID channelUID, @Nullable String value) {
updateState(channelUID, new StringType(value));
}
protected void updateDecimalTypeChannel(ChannelUID channelUID, @Nullable Number value) {
if (value == null) {
return;

View File

@ -60,7 +60,7 @@ public class SensorThingHandler extends SensorBaseThingHandler {
THING_TYPE_TEMPERATURE_SENSOR, THING_TYPE_HUMIDITY_SENSOR, THING_TYPE_PRESSURE_SENSOR, THING_TYPE_SWITCH,
THING_TYPE_OPENCLOSE_SENSOR, THING_TYPE_WATERLEAKAGE_SENSOR, THING_TYPE_FIRE_SENSOR,
THING_TYPE_ALARM_SENSOR, THING_TYPE_VIBRATION_SENSOR, THING_TYPE_BATTERY_SENSOR,
THING_TYPE_CARBONMONOXIDE_SENSOR, THING_TYPE_COLOR_CONTROL);
THING_TYPE_CARBONMONOXIDE_SENSOR, THING_TYPE_AIRQUALITY_SENSOR, THING_TYPE_COLOR_CONTROL);
private static final List<String> CONFIG_CHANNELS = List.of(CHANNEL_BATTERY_LEVEL, CHANNEL_BATTERY_LOW,
CHANNEL_ENABLED, CHANNEL_TEMPERATURE);
@ -196,6 +196,12 @@ public class SensorThingHandler extends SensorBaseThingHandler {
case CHANNEL_CARBONMONOXIDE:
updateSwitchChannel(channelUID, newState.carbonmonoxide);
break;
case CHANNEL_AIRQUALITY:
updateStringChannel(channelUID, newState.airquality);
break;
case CHANNEL_AIRQUALITYPPB:
updateDecimalTypeChannel(channelUID, newState.airqualityppb);
break;
case CHANNEL_BUTTON:
updateDecimalTypeChannel(channelUID, newState.buttonevent);
break;

View File

@ -10,6 +10,8 @@ thing-type.deconz.alarmsensor.description = An alarm sensor
thing-type.deconz.batterysensor.label = Battery Sensor
thing-type.deconz.batterysensor.description = A battery sensor
thing-type.deconz.carbonmonoxidesensor.label = Carbon-monoxide Sensor
thing-type.deconz.airqualitysensor.label = Air quality Sensor
thing-type.deconz.airqualitysensor.description = An air quality sensor
thing-type.deconz.colorcontrol.label = Color Controller
thing-type.deconz.colorlight.label = Color Light
thing-type.deconz.colorlight.description = A dimmable light with adjustable color.
@ -112,6 +114,10 @@ channel-type.deconz.buttonevent.label = Button Trigger
channel-type.deconz.buttonevent.description = This channel is triggered on a button event. The trigger payload consists of the button event number.
channel-type.deconz.carbonmonoxide.label = Carbon-monoxide
channel-type.deconz.carbonmonoxide.description = Carbon-monoxide was detected.
channel-type.deconz.airquality.label = Air quality level
channel-type.deconz.airquality.description = Current air quality level based on volatile organic compounds (VOCs) measurement. Example: good or poor, ...
channel-type.deconz.airqualityppb.label = Air quality in ppb
channel-type.deconz.airqualityppb.description = Current air quality based on measurements of volatile organic compounds (VOCs). The measured value is specified in ppb (parts per billion).
channel-type.deconz.consumption.label = Consumption
channel-type.deconz.consumption.description = Current consumption
channel-type.deconz.ct.label = Color Temperature

View File

@ -10,6 +10,8 @@ thing-type.deconz.alarmsensor.description = Ein Alarmsensor
thing-type.deconz.batterysensor.label = Batteriesensor
thing-type.deconz.batterysensor.description = Ein Batteriesensor
thing-type.deconz.carbonmonoxidesensor.label = Kohlenmonoxid-Sensor
thing-type.deconz.airqualitysensor.label = Luftqualit\u00e4tssensor
thing-type.deconz.airqualitysensor.description = Ein Luftqualit\u00e4tssensor
thing-type.deconz.colorcontrol.label = Farbregler
thing-type.deconz.colorlight.label = Farbige Lampe
thing-type.deconz.colorlight.description = Dimmbare Lampe mit einstellbarer Farbe.
@ -112,6 +114,10 @@ channel-type.deconz.buttonevent.label = Schaltflächen-Trigger
channel-type.deconz.buttonevent.description = Dieser Channel wird bei Bestätigung einer Schaltfläche ausgelöst. Der Trigger-Payload besteht aus der Nummer der Schaltfläche.
channel-type.deconz.carbonmonoxide.label = Kohlenmonoxid
channel-type.deconz.carbonmonoxide.description = Zeigt an, ob ein erhöhter Kohlenmonoxid-Wert erkannt wurde.
channel-type.deconz.airquality.label = Luftqualit\u00e4t
channel-type.deconz.airquality.description = Derzeitige Luftqualit\u00e4t auf der Grundlage von Messungen fl\u00fcchtiger organischer Verbindungen (VOCs). Beispiel: good oder poor, ...
channel-type.deconz.airqualityppb.label = Luftqualit\u00e4t in ppb
channel-type.deconz.airqualityppb.description = Derzeitige Luftqualit\u00e4t auf der Grundlage von Messungen der fl\u00fcchtigen organischen Verbindungen (VOC). Der Messwert wird in ppb (parts per billion) angegeben.
channel-type.deconz.consumption.label = Verbrauch
channel-type.deconz.consumption.description = Zeigt den aktuellen Verbrauch an.
channel-type.deconz.ct.label = Farbtemperatur

View File

@ -491,6 +491,41 @@
<state readOnly="true"/>
</channel-type>
<thing-type id="airqualitysensor">
<supported-bridge-type-refs>
<bridge-type-ref id="deconz"/>
</supported-bridge-type-refs>
<label>Air quality Sensor</label>
<description>An air quality sensor</description>
<channels>
<channel typeId="airquality" id="airquality"/>
<channel typeId="airqualityppb" id="airqualityppb"/>
<channel typeId="last_updated" id="last_updated"/>
</channels>
<representation-property>uid</representation-property>
<config-description-ref uri="thing-type:deconz:sensor"/>
</thing-type>
<channel-type id="airquality">
<item-type>String</item-type>
<label>Air quality level</label>
<description>Current air quality level based on volatile organic compounds (VOCs) measurement. Example: good or poor,
...</description>
<state readOnly="true" pattern="%s"></state>
</channel-type>
<channel-type id="airqualityppb">
<item-type>Number:Dimensionless</item-type>
<label>Air quality in ppb</label>
<description>Current air quality based on measurements of volatile organic compounds (VOCs). The measured value is
specified in ppb (parts per billion).</description>
<state readOnly="true" pattern="%d"></state>
</channel-type>
<thing-type id="thermostat">
<supported-bridge-type-refs>
<bridge-type-ref id="deconz"/>

View File

@ -53,6 +53,7 @@ import com.google.gson.GsonBuilder;
*
* @author Jan N. Klug - Initial contribution
* @author Lukas Agethen - Added Thermostat
* @author Philipp Schneider - Added air quality sensor
*/
@ExtendWith(MockitoExtension.class)
@NonNullByDefault
@ -85,6 +86,46 @@ public class SensorsTest {
Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelUID), eq(OnOffType.ON));
}
@Test
public void airQualitySensorUpdateTest() throws IOException {
// ARRANGE
SensorMessage sensorMessage = DeconzTest.getObjectFromJson("airquality.json", SensorMessage.class, gson);
assertNotNull(sensorMessage);
ThingUID thingUID = new ThingUID("deconz", "sensor");
ChannelUID channelUID = new ChannelUID(thingUID, "airquality");
Thing sensor = ThingBuilder.create(THING_TYPE_AIRQUALITY_SENSOR, thingUID)
.withChannel(ChannelBuilder.create(channelUID, "String").build()).build();
SensorThingHandler sensorThingHandler = new SensorThingHandler(sensor, gson);
sensorThingHandler.setCallback(thingHandlerCallback);
// ACT
sensorThingHandler.messageReceived("", sensorMessage);
// ASSERT
Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelUID), eq(StringType.valueOf("good")));
}
@Test
public void airQualityPpbSensorUpdateTest() throws IOException {
// ARRANGE
SensorMessage sensorMessage = DeconzTest.getObjectFromJson("airquality.json", SensorMessage.class, gson);
assertNotNull(sensorMessage);
ThingUID thingUID = new ThingUID("deconz", "sensor");
ChannelUID channelUID = new ChannelUID(thingUID, "airqualityppb");
Thing sensor = ThingBuilder.create(THING_TYPE_AIRQUALITY_SENSOR, thingUID)
.withChannel(ChannelBuilder.create(channelUID, "Number").build()).build();
SensorThingHandler sensorThingHandler = new SensorThingHandler(sensor, gson);
sensorThingHandler.setCallback(thingHandlerCallback);
// ACT
sensorThingHandler.messageReceived("", sensorMessage);
// ASSERT
Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelUID), eq(new DecimalType(129)));
}
@Test
public void thermostatSensorUpdateTest() throws IOException {
SensorMessage sensorMessage = DeconzTest.getObjectFromJson("thermostat.json", SensorMessage.class, gson);

View File

@ -0,0 +1,22 @@
{
"config": {
"battery": 100,
"on": true,
"reachable": true
},
"ep": 38,
"etag": "ca904b42f63d0ccccccccccccccccccccc",
"lastannounced": "2021-12-28T23:59:02Z",
"lastseen": "2021-12-29T01:18Z",
"manufacturername": "Develco Products A/S",
"modelid": "AQSZB-110",
"name": "AirQuality 4",
"state": {
"airquality": "good",
"airqualityppb": 129,
"lastupdated": "2021-12-29T01:18:41.184"
},
"swversion": "2021-10-28 08:59",
"type": "ZHAAirQuality",
"uniqueid": "00:00:00:00:00:00:00:00-00-fc03"
}