Skip to content

Commit

Permalink
[Netatmo] Adding Carbon Monoxide sensor (#14543)
Browse files Browse the repository at this point in the history
* Added Carbon Monoxide detector

---------

Signed-off-by: clinique <[email protected]>
  • Loading branch information
clinique authored Mar 8, 2023
1 parent 50cdd02 commit 90b2279
Show file tree
Hide file tree
Showing 15 changed files with 128 additions and 93 deletions.
19 changes: 19 additions & 0 deletions bundles/org.openhab.binding.netatmo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ The Netatmo binding integrates the following Netatmo products:
- _Doorbell_
- _Smoke Detector_
- _Smart Door Sensor_
- _Carbon Monoxide Detector_

See <https://www.netatmo.com/> for details on their product.

Expand Down Expand Up @@ -94,6 +95,8 @@ Now that you have got your bridge _ONLINE_ you can now start a scan with the bin
| room | Thing | NARoom | A room in your house. | id |
| valve | Thing | NRV | A valve controlling a radiator. | id |
| tag | Thing | NACamDoorTag | A door / window sensor | id |
| smoke-detector | Thing | NSD | A Smoke Detector | id |
| co-detector | Thing | NCO | A Carbon Monoxide Alarm | id |

### Webhook

Expand Down Expand Up @@ -642,6 +645,22 @@ All these channels are read only.
| last-event | subtype | String | Sub-type of event |
| last-event | message | String | Last event message from this person |

### Netatmo Smart Carbon Monoxide Detector

All these channels are read only.

**Supported channels for the Carbon Monoxide Detector thing:**

| Channel Group | Channel Id | Item Type | Description |
| ------------- | ---------- | ------------ | ------------------------------------------------ |
| signal | strength | Number | Signal strength (0 for no signal, 1 for weak...) |
| signal | value | Number:Power | Signal strength in dBm |
| timestamp | last-seen | DateTime | Last time the module reported its presence |
| last-event | type | String | Type of event |
| last-event | time | DateTime | Moment of the last event for this detector |
| last-event | subtype | String | Sub-type of event |
| last-event | message | String | Last event message from this detector |

## Configuration Examples

### things/netatmo.things
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,9 @@ public class NetatmoBindingConstants {
public static final String OPTION_PERSON = "-person";
public static final String OPTION_ROOM = "-room";
public static final String OPTION_THERMOSTAT = "-thermostat";
public static final String OPTION_SMOKE = "-smoke";
public static final String OPTION_ALARM = "-alarm";
public static final Set<String> GROUP_VARIATIONS = Set.of(OPTION_EXTENDED, OPTION_OUTSIDE, OPTION_DOORBELL,
OPTION_PERSON, OPTION_ROOM, OPTION_THERMOSTAT, OPTION_SMOKE);
OPTION_PERSON, OPTION_ROOM, OPTION_THERMOSTAT, OPTION_ALARM);

public static final String GROUP_TYPE_TIMESTAMP_EXTENDED = GROUP_TIMESTAMP + OPTION_EXTENDED;
public static final String GROUP_TYPE_BATTERY_EXTENDED = GROUP_BATTERY + OPTION_EXTENDED;
Expand All @@ -83,7 +83,7 @@ public class NetatmoBindingConstants {
public static final String GROUP_DOORBELL_LAST_EVENT = GROUP_LAST_EVENT + OPTION_DOORBELL;
public static final String GROUP_DOORBELL_SUB_EVENT = GROUP_SUB_EVENT + OPTION_DOORBELL;
public static final String GROUP_PERSON_LAST_EVENT = GROUP_LAST_EVENT + OPTION_PERSON;
public static final String GROUP_SMOKE_LAST_EVENT = GROUP_LAST_EVENT + OPTION_SMOKE;
public static final String GROUP_ALARM_LAST_EVENT = GROUP_LAST_EVENT + OPTION_ALARM;
public static final String GROUP_TYPE_ROOM_TEMPERATURE = GROUP_TEMPERATURE + OPTION_ROOM;
public static final String GROUP_TYPE_ROOM_PROPERTIES = GROUP_PROPERTIES + OPTION_ROOM;
public static final String GROUP_TYPE_TH_PROPERTIES = GROUP_PROPERTIES + OPTION_THERMOSTAT;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.netatmo.internal.api.data.ChannelGroup;
import org.openhab.binding.netatmo.internal.api.data.ModuleType;
import org.openhab.binding.netatmo.internal.config.BindingConfiguration;
import org.openhab.binding.netatmo.internal.deserialization.NADeserializer;
Expand All @@ -27,6 +28,7 @@
import org.openhab.binding.netatmo.internal.handler.DeviceHandler;
import org.openhab.binding.netatmo.internal.handler.ModuleHandler;
import org.openhab.binding.netatmo.internal.handler.capability.AirCareCapability;
import org.openhab.binding.netatmo.internal.handler.capability.AlarmEventCapability;
import org.openhab.binding.netatmo.internal.handler.capability.CameraCapability;
import org.openhab.binding.netatmo.internal.handler.capability.Capability;
import org.openhab.binding.netatmo.internal.handler.capability.ChannelHelperCapability;
Expand All @@ -37,7 +39,6 @@
import org.openhab.binding.netatmo.internal.handler.capability.PersonCapability;
import org.openhab.binding.netatmo.internal.handler.capability.PresenceCapability;
import org.openhab.binding.netatmo.internal.handler.capability.RoomCapability;
import org.openhab.binding.netatmo.internal.handler.capability.SmokeCapability;
import org.openhab.binding.netatmo.internal.handler.capability.WeatherCapability;
import org.openhab.binding.netatmo.internal.handler.channelhelper.ChannelHelper;
import org.openhab.binding.netatmo.internal.providers.NetatmoDescriptionProvider;
Expand Down Expand Up @@ -113,8 +114,8 @@ private BaseThingHandler buildHandler(Thing thing, ModuleType moduleType) {
CommonInterface handler = moduleType.isABridge() ? new DeviceHandler((Bridge) thing) : new ModuleHandler(thing);

List<ChannelHelper> helpers = new ArrayList<>();
moduleType.channelGroups
.forEach(channelGroup -> channelGroup.getHelperInstance().ifPresent(helper -> helpers.add(helper)));

helpers.addAll(moduleType.channelGroups.stream().map(ChannelGroup::getHelperInstance).toList());

moduleType.capabilities.forEach(capability -> {
Capability newCap = null;
Expand All @@ -134,8 +135,8 @@ private BaseThingHandler buildHandler(Thing thing, ModuleType moduleType) {
newCap = new PersonCapability(handler, stateDescriptionProvider, helpers);
} else if (capability == CameraCapability.class) {
newCap = new CameraCapability(handler, stateDescriptionProvider, helpers);
} else if (capability == SmokeCapability.class) {
newCap = new SmokeCapability(handler, stateDescriptionProvider, helpers);
} else if (capability == AlarmEventCapability.class) {
newCap = new AlarmEventCapability(handler, stateDescriptionProvider, helpers);
} else if (capability == PresenceCapability.class) {
newCap = new PresenceCapability(handler, stateDescriptionProvider, helpers);
} else if (capability == MeasureCapability.class) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@

import static org.openhab.binding.netatmo.internal.NetatmoBindingConstants.*;

import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

Expand All @@ -32,8 +31,6 @@
import org.openhab.binding.netatmo.internal.handler.channelhelper.TemperatureChannelHelper;
import org.openhab.binding.netatmo.internal.handler.channelhelper.TimestampChannelHelper;
import org.openhab.binding.netatmo.internal.providers.NetatmoThingTypeProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* The {@link ChannelGroup} makes the link between a channel helper and some group types. It also
Expand Down Expand Up @@ -66,8 +63,9 @@ public class ChannelGroup {
GROUP_NOISE);
public static final ChannelGroup HUMIDITY = new ChannelGroup(HumidityChannelHelper.class, MeasureClass.HUMIDITY,
GROUP_HUMIDITY);
public static final ChannelGroup ALARM_LAST_EVENT = new ChannelGroup(EventChannelHelper.class,
GROUP_ALARM_LAST_EVENT);

private final Logger logger = LoggerFactory.getLogger(ChannelGroup.class);
private final Class<? extends ChannelHelper> helper;
public final Set<String> groupTypes;
public final Set<String> extensions;
Expand All @@ -86,13 +84,13 @@ private ChannelGroup(Class<? extends ChannelHelper> helper, Set<String> extensio
this.extensions = extensions;
}

public Optional<ChannelHelper> getHelperInstance() {
public ChannelHelper getHelperInstance() {
try {
return Optional.of(helper.getConstructor(Set.class).newInstance(
groupTypes.stream().map(NetatmoThingTypeProvider::toGroupName).collect(Collectors.toSet())));
return helper.getConstructor(Set.class).newInstance(
groupTypes.stream().map(NetatmoThingTypeProvider::toGroupName).collect(Collectors.toSet()));
} catch (ReflectiveOperationException e) {
logger.warn("Error creating or initializing helper class : {}", e.getMessage());
throw new IllegalArgumentException(
"Error creating or initializing helper class : %s".formatted(e.getMessage()));
}
return Optional.empty();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public enum EventSubType {
SD_CARD_INCOMPATIBLE_SPEED(6, EventType.SD),
SD_CARD_INSUFFICIENT_SPACE(7, EventType.SD),

// Alimentation sub events
// Power sub events
ALIM_INCORRECT_POWER(1, EventType.ALIM),
ALIM_CORRECT_POWER(2, EventType.ALIM),

Expand All @@ -47,6 +47,12 @@ public enum EventSubType {
SOUND_TEST_ERROR(1, EventType.SOUND_TEST),
DETECTOR_READY(0, EventType.TAMPERED),
DETECTOR_TAMPERED(1, EventType.TAMPERED),

// Carbon Monoxide Alarm
CO_OK(0, EventType.CO_DETECTED),
CO_PRE_ALARM(1, EventType.CO_DETECTED),
CO_ALARM(2, EventType.CO_DETECTED),

WIFI_STATUS_OK(1, EventType.WIFI_STATUS),
WIFI_STATUS_ERROR(0, EventType.WIFI_STATUS),

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,22 +111,25 @@ public enum EventType {
SMOKE(ModuleType.SMOKE_DETECTOR),

@SerializedName("tampered") // When smoke detector is ready or tampered
TAMPERED(ModuleType.SMOKE_DETECTOR),
TAMPERED(ModuleType.SMOKE_DETECTOR, ModuleType.CO_DETECTOR),

@SerializedName("wifi_status") // When wifi status is updated
WIFI_STATUS(ModuleType.SMOKE_DETECTOR),
WIFI_STATUS(ModuleType.SMOKE_DETECTOR, ModuleType.CO_DETECTOR),

@SerializedName("battery_status") // When battery status is too low
BATTERY_STATUS(ModuleType.SMOKE_DETECTOR),
BATTERY_STATUS(ModuleType.SMOKE_DETECTOR, ModuleType.CO_DETECTOR),

@SerializedName("detection_chamber_status") // When the detection chamber is dusty or clean
DETECTION_CHAMBER_STATUS(ModuleType.SMOKE_DETECTOR),

@SerializedName("sound_test") // Sound test result
SOUND_TEST(ModuleType.SMOKE_DETECTOR),
SOUND_TEST(ModuleType.SMOKE_DETECTOR, ModuleType.CO_DETECTOR),

@SerializedName("new_device")
NEW_DEVICE(ModuleType.HOME);
NEW_DEVICE(ModuleType.HOME),

@SerializedName("co_detected")
CO_DETECTED(ModuleType.CO_DETECTOR);

public static final EnumSet<EventType> AS_SET = EnumSet.allOf(EventType.class);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,15 @@
import java.net.URI;
import java.util.EnumSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.netatmo.internal.api.data.NetatmoConstants.FeatureArea;
import org.openhab.binding.netatmo.internal.api.data.NetatmoConstants.MeasureClass;
import org.openhab.binding.netatmo.internal.handler.capability.AirCareCapability;
import org.openhab.binding.netatmo.internal.handler.capability.AlarmEventCapability;
import org.openhab.binding.netatmo.internal.handler.capability.CameraCapability;
import org.openhab.binding.netatmo.internal.handler.capability.Capability;
import org.openhab.binding.netatmo.internal.handler.capability.ChannelHelperCapability;
Expand All @@ -36,14 +37,12 @@
import org.openhab.binding.netatmo.internal.handler.capability.PersonCapability;
import org.openhab.binding.netatmo.internal.handler.capability.PresenceCapability;
import org.openhab.binding.netatmo.internal.handler.capability.RoomCapability;
import org.openhab.binding.netatmo.internal.handler.capability.SmokeCapability;
import org.openhab.binding.netatmo.internal.handler.capability.WeatherCapability;
import org.openhab.binding.netatmo.internal.handler.channelhelper.AirQualityChannelHelper;
import org.openhab.binding.netatmo.internal.handler.channelhelper.ApiBridgeChannelHelper;
import org.openhab.binding.netatmo.internal.handler.channelhelper.CameraChannelHelper;
import org.openhab.binding.netatmo.internal.handler.channelhelper.DoorTagChannelHelper;
import org.openhab.binding.netatmo.internal.handler.channelhelper.EnergyChannelHelper;
import org.openhab.binding.netatmo.internal.handler.channelhelper.EventChannelHelper;
import org.openhab.binding.netatmo.internal.handler.channelhelper.EventDoorbellChannelHelper;
import org.openhab.binding.netatmo.internal.handler.channelhelper.EventPersonChannelHelper;
import org.openhab.binding.netatmo.internal.handler.channelhelper.PersonChannelHelper;
Expand All @@ -59,13 +58,14 @@
import org.openhab.core.thing.ThingTypeUID;

/**
* This enum all handled Netatmo modules and devices along with their capabilities
* This enum describes all Netatmo modules and devices along with their capabilities.
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public enum ModuleType {
UNKNOWN(FeatureArea.NONE, "", null, Set.of()),

ACCOUNT(FeatureArea.NONE, "", null, Set.of(), new ChannelGroup(ApiBridgeChannelHelper.class, GROUP_MONITORING)),

HOME(FeatureArea.NONE, "NAHome", ACCOUNT,
Expand Down Expand Up @@ -140,13 +140,15 @@ ChannelGroup.BATTERY_EXT, new ChannelGroup(Therm1ChannelHelper.class, GROUP_TYPE
new ChannelGroup(RoomChannelHelper.class, GROUP_TYPE_ROOM_PROPERTIES, GROUP_TYPE_ROOM_TEMPERATURE),
new ChannelGroup(SetpointChannelHelper.class, GROUP_SETPOINT)),

SMOKE_DETECTOR(FeatureArea.SECURITY, "NSD", HOME, Set.of(SmokeCapability.class, ChannelHelperCapability.class),
ChannelGroup.SIGNAL, ChannelGroup.TIMESTAMP,
new ChannelGroup(EventChannelHelper.class, GROUP_SMOKE_LAST_EVENT));
SMOKE_DETECTOR(FeatureArea.SECURITY, "NSD", HOME, Set.of(AlarmEventCapability.class, ChannelHelperCapability.class),
ChannelGroup.SIGNAL, ChannelGroup.TIMESTAMP, ChannelGroup.ALARM_LAST_EVENT),

CO_DETECTOR(FeatureArea.SECURITY, "NCO", HOME, Set.of(AlarmEventCapability.class, ChannelHelperCapability.class),
ChannelGroup.SIGNAL, ChannelGroup.TIMESTAMP, ChannelGroup.ALARM_LAST_EVENT);

public static final EnumSet<ModuleType> AS_SET = EnumSet.allOf(ModuleType.class);

private final @Nullable ModuleType bridgeType;
private final Optional<ModuleType> bridgeType;
public final Set<ChannelGroup> channelGroups;
public final Set<Class<? extends Capability>> capabilities;
public final ThingTypeUID thingTypeUID;
Expand All @@ -155,7 +157,7 @@ ChannelGroup.BATTERY_EXT, new ChannelGroup(Therm1ChannelHelper.class, GROUP_TYPE

ModuleType(FeatureArea feature, String apiName, @Nullable ModuleType bridge,
Set<Class<? extends Capability>> capabilities, ChannelGroup... channelGroups) {
this.bridgeType = bridge;
this.bridgeType = Optional.ofNullable(bridge);
this.feature = feature;
this.capabilities = capabilities;
this.apiName = apiName;
Expand All @@ -167,21 +169,16 @@ public boolean isLogical() {
return !channelGroups.contains(ChannelGroup.SIGNAL);
}

public boolean isABridge() {
for (ModuleType mt : ModuleType.values()) {
if (this.equals(mt.bridgeType)) {
return true;
}
}
return false;
public boolean isABridge() { // I am a bridge if any module references me as being so
return AS_SET.stream().anyMatch(mt -> this.equals(mt.getBridge()));
}

public List<String> getExtensions() {
return channelGroups.stream().map(cg -> cg.extensions).flatMap(Set::stream).collect(Collectors.toList());
return channelGroups.stream().map(cg -> cg.extensions).flatMap(Set::stream).toList();
}

public Set<String> getGroupTypes() {
return channelGroups.stream().map(cg -> cg.groupTypes).flatMap(Set::stream).collect(Collectors.toSet());
public List<String> getGroupTypes() {
return channelGroups.stream().map(cg -> cg.groupTypes).flatMap(Set::stream).toList();
}

public int[] getSignalLevels() {
Expand All @@ -191,29 +188,29 @@ public int[] getSignalLevels() {
: WIFI_SIGNAL_LEVELS;
}
throw new IllegalArgumentException(
"This should not be called for module type : " + name() + ", please file a bug report.");
"getSignalLevels should not be called for module type : '%s', please file a bug report."
.formatted(name()));
}

public ModuleType getBridge() {
ModuleType bridge = bridgeType;
return bridge != null ? bridge : ModuleType.UNKNOWN;
return bridgeType.orElse(UNKNOWN);
}

public URI getConfigDescription() {
return URI.create(BINDING_ID + ":"
+ (equals(ACCOUNT) ? "api_bridge"
: equals(HOME) ? "home"
: (isLogical() ? "virtual"
: ModuleType.UNKNOWN.equals(getBridge()) ? "configurable" : "device")));
: (isLogical() ? "virtual" : UNKNOWN.equals(getBridge()) ? "configurable" : "device")));
}

public int getDepth() {
ModuleType parent = bridgeType;
return parent == null ? 1 : 1 + parent.getDepth();
ModuleType parent = getBridge();
return parent == UNKNOWN ? 1 : parent.getDepth() + 1;
}

public static ModuleType from(ThingTypeUID thingTypeUID) {
return ModuleType.AS_SET.stream().filter(mt -> mt.thingTypeUID.equals(thingTypeUID)).findFirst()
.orElseThrow(() -> new IllegalArgumentException());
return AS_SET.stream().filter(mt -> mt.thingTypeUID.equals(thingTypeUID)).findFirst()
.orElseThrow(() -> new IllegalArgumentException(
"No known ModuleType matched '%s'".formatted(thingTypeUID.toString())));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -197,10 +197,13 @@ public static enum Scope {
WRITE_DOORBELL,
@SerializedName("access_doorbell")
ACCESS_DOORBELL,
@SerializedName("read_carbonmonoxidedetector")
READ_CARBONMONOXIDEDETECTOR,
UNKNOWN;
}

private static final Scope[] SMOKE_SCOPES = { Scope.READ_SMOKEDETECTOR };
private static final Scope[] CARBON_MONOXIDE_SCOPES = { Scope.READ_CARBONMONOXIDEDETECTOR };
private static final Scope[] AIR_CARE_SCOPES = { Scope.READ_HOMECOACH };
private static final Scope[] WEATHER_SCOPES = { Scope.READ_STATION };
private static final Scope[] THERMOSTAT_SCOPES = { Scope.READ_THERMOSTAT, Scope.WRITE_THERMOSTAT };
Expand All @@ -212,7 +215,7 @@ public static enum FeatureArea {
AIR_CARE(AIR_CARE_SCOPES),
WEATHER(WEATHER_SCOPES),
ENERGY(THERMOSTAT_SCOPES),
SECURITY(WELCOME_SCOPES, PRESENCE_SCOPES, SMOKE_SCOPES, DOORBELL_SCOPES),
SECURITY(WELCOME_SCOPES, PRESENCE_SCOPES, SMOKE_SCOPES, DOORBELL_SCOPES, CARBON_MONOXIDE_SCOPES),
NONE();

public static String ALL_SCOPES = EnumSet.allOf(FeatureArea.class).stream().map(fa -> fa.scopes)
Expand Down
Loading

0 comments on commit 90b2279

Please sign in to comment.