]> git.basschouten.com Git - openhab-addons.git/commitdiff
[Netatmo] Adding Carbon Monoxide sensor (#14543)
authorGaël L'hopital <gael@lhopital.org>
Wed, 8 Mar 2023 18:27:35 +0000 (19:27 +0100)
committerGitHub <noreply@github.com>
Wed, 8 Mar 2023 18:27:35 +0000 (19:27 +0100)
* Added Carbon Monoxide detector

---------

Signed-off-by: clinique <gael@lhopital.org>
16 files changed:
bundles/org.openhab.binding.netatmo/README.md
bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/NetatmoBindingConstants.java
bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/NetatmoHandlerFactory.java
bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/api/data/ChannelGroup.java
bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/api/data/EventSubType.java
bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/api/data/EventType.java
bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/api/data/ModuleType.java
bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/api/data/NetatmoConstants.java
bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/ApiBridgeHandler.java
bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/capability/AlarmEventCapability.java [new file with mode: 0644]
bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/capability/CameraCapability.java
bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/capability/SecurityCapability.java
bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/capability/SmokeCapability.java [deleted file]
bundles/org.openhab.binding.netatmo/src/main/resources/OH-INF/i18n/netatmo.properties
bundles/org.openhab.binding.netatmo/src/main/resources/OH-INF/thing/channels.xml
bundles/org.openhab.binding.netatmo/src/main/resources/OH-INF/thing/security.xml

index f0def84b734b4444bd0be79d35c050cd8659637c..a2151ff80f3258c68bd72d51e49fb83c686503b0 100644 (file)
@@ -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.
 
@@ -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
 
@@ -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
index a728889a90a6bee992ead83337b5acf70aabeb5a..2f8ef0636b58d9d39ffcdc47c5e600482588f8b1 100644 (file)
@@ -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;
@@ -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;
index 4053372bf3cff5efbabbc0c2570a05c9bc2ef347..3d85c3e1f1984052a918c44cc39bb297043197f7 100644 (file)
@@ -19,6 +19,7 @@ import java.util.Map;
 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;
@@ -27,6 +28,7 @@ import org.openhab.binding.netatmo.internal.handler.CommonInterface;
 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;
@@ -37,7 +39,6 @@ import org.openhab.binding.netatmo.internal.handler.capability.MeasureCapability
 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;
@@ -113,8 +114,8 @@ public class NetatmoHandlerFactory extends BaseThingHandlerFactory {
         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;
@@ -134,8 +135,8 @@ public class NetatmoHandlerFactory extends BaseThingHandlerFactory {
                 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) {
index 0b3fa8ab095705faca10df0adbbee1d167b0fd10..c3cc59c2428dcea4d3dead714e21cfbfae74dc31 100644 (file)
@@ -14,7 +14,6 @@ package org.openhab.binding.netatmo.internal.api.data;
 
 import static org.openhab.binding.netatmo.internal.NetatmoBindingConstants.*;
 
-import java.util.Optional;
 import java.util.Set;
 import java.util.stream.Collectors;
 
@@ -32,8 +31,6 @@ import org.openhab.binding.netatmo.internal.handler.channelhelper.SignalChannelH
 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
@@ -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;
@@ -86,13 +84,13 @@ public class ChannelGroup {
         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();
     }
 }
index 7c937b979eef52e63adbe42a51803400c7c4b24b..7cf497e66c86da582c69982d8545dffaeff8ad4e 100644 (file)
@@ -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),
 
@@ -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),
 
index 5e383efae325335524dabc44b8b7d4ffcf501c03..dfb8de510a7deab8e1669a670e81fa2f01311b79 100644 (file)
@@ -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);
 
index 2314d1092c1d3eab0f9629d3c1b497528eb1b904..de5e14d9b522662ea88523f3442a149b949b5fa6 100644 (file)
@@ -18,14 +18,15 @@ import static org.openhab.binding.netatmo.internal.api.data.NetatmoConstants.*;
 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;
@@ -36,14 +37,12 @@ import org.openhab.binding.netatmo.internal.handler.capability.MeasureCapability
 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;
@@ -59,13 +58,14 @@ import org.openhab.binding.netatmo.internal.handler.channelhelper.WindChannelHel
 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,
@@ -140,13 +140,15 @@ public enum ModuleType {
             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;
@@ -155,7 +157,7 @@ public enum ModuleType {
 
     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;
@@ -167,21 +169,16 @@ public enum ModuleType {
         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() {
@@ -191,29 +188,29 @@ public enum ModuleType {
                     : 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())));
     }
 }
index c805180223b1db7042a7870ce5f866e3f3b97dc4..e2d692a3fea29d1b88a34094a2da8167db6ab51b 100644 (file)
@@ -197,10 +197,13 @@ public class NetatmoConstants {
         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 };
@@ -212,7 +215,7 @@ public class NetatmoConstants {
         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)
index 52018abb7f576d875884df6fccdf593754e3d260..d210d91de3bca32d92bad03ca30be77a2fe134db 100644 (file)
@@ -43,6 +43,7 @@ import org.eclipse.jetty.client.HttpClient;
 import org.eclipse.jetty.client.api.ContentResponse;
 import org.eclipse.jetty.client.api.Request;
 import org.eclipse.jetty.client.util.InputStreamContentProvider;
+import org.eclipse.jetty.http.HttpField;
 import org.eclipse.jetty.http.HttpHeader;
 import org.eclipse.jetty.http.HttpMethod;
 import org.eclipse.jetty.http.HttpStatus;
@@ -150,6 +151,7 @@ public class ApiBridgeHandler extends BaseBridgeHandler {
                     String refreshToken = connectApi.authorize(configuration, code, redirectUri);
 
                     if (configuration.refreshToken.isBlank()) {
+                        logger.trace("Adding refresh token to configuration : {}", refreshToken);
                         Configuration thingConfig = editConfiguration();
                         thingConfig.put(ApiHandlerConfiguration.REFRESH_TOKEN, refreshToken);
                         updateConfiguration(thingConfig);
@@ -254,6 +256,7 @@ public class ApiBridgeHandler extends BaseBridgeHandler {
                 try (InputStreamContentProvider inputStreamContentProvider = new InputStreamContentProvider(stream)) {
                     request.content(inputStreamContentProvider, contentType);
                 }
+                logger.trace(" -with payload : {} ", payload);
             }
 
             if (isLinked(requestCountChannelUID)) {
@@ -265,22 +268,25 @@ public class ApiBridgeHandler extends BaseBridgeHandler {
                 }
                 updateState(requestCountChannelUID, new DecimalType(requestsTimestamps.size()));
             }
+            logger.trace(" -with headers : {} ",
+                    String.join(", ", request.getHeaders().stream().map(HttpField::toString).toList()));
             ContentResponse response = request.send();
 
             Code statusCode = HttpStatus.getCode(response.getStatus());
             String responseBody = new String(response.getContent(), StandardCharsets.UTF_8);
-            logger.trace("executeUri returned : code {} body {}", statusCode, responseBody);
+            logger.trace(" -returned : code {} body {}", statusCode, responseBody);
 
-            if (statusCode != Code.OK) {
-                try {
-                    ApiError error = deserializer.deserialize(ApiError.class, responseBody);
-                    throw new NetatmoException(error);
-                } catch (NetatmoException e) {
-                    logger.debug("Error deserializing payload from error response", e);
-                    throw new NetatmoException(statusCode.getMessage());
-                }
+            if (statusCode == Code.OK) {
+                return deserializer.deserialize(clazz, responseBody);
+            }
+
+            NetatmoException exception;
+            try {
+                exception = new NetatmoException(deserializer.deserialize(ApiError.class, responseBody));
+            } catch (NetatmoException e) {
+                exception = new NetatmoException("Error deserializing error : %s".formatted(statusCode.getMessage()));
             }
-            return deserializer.deserialize(clazz, responseBody);
+            throw exception;
         } catch (NetatmoException e) {
             if (e.getStatusCode() == ServiceError.MAXIMUM_USAGE_REACHED) {
                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
diff --git a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/capability/AlarmEventCapability.java b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/capability/AlarmEventCapability.java
new file mode 100644 (file)
index 0000000..44005b0
--- /dev/null
@@ -0,0 +1,42 @@
+/**
+ * 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.netatmo.internal.handler.capability;
+
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.netatmo.internal.api.dto.NAObject;
+import org.openhab.binding.netatmo.internal.handler.CommonInterface;
+import org.openhab.binding.netatmo.internal.handler.channelhelper.ChannelHelper;
+import org.openhab.binding.netatmo.internal.providers.NetatmoDescriptionProvider;
+
+/**
+ * {@link AlarmEventCapability} gives the ability to handle Alarm modules events
+ *
+ * @author Gaël L'hopital - Initial contribution
+ *
+ */
+@NonNullByDefault
+public class AlarmEventCapability extends HomeSecurityThingCapability {
+
+    public AlarmEventCapability(CommonInterface handler, NetatmoDescriptionProvider descriptionProvider,
+            List<ChannelHelper> channelHelpers) {
+        super(handler, descriptionProvider, channelHelpers);
+    }
+
+    @Override
+    public List<NAObject> updateReadings() {
+        return securityCapability.map(cap -> cap.getDeviceLastEvent(handler.getId(), moduleType.apiName))
+                .map(event -> List.of((NAObject) event)).orElse(List.of());
+    }
+}
index 8e9da2246950e727893db6a4de8c05023c7fbe9b..35b25ad63bce5469d2990b3d9d9f12d66679c1f8 100644 (file)
@@ -100,7 +100,7 @@ public class CameraCapability extends HomeSecurityThingCapability {
     public List<NAObject> updateReadings() {
         List<NAObject> result = new ArrayList<>();
         securityCapability.ifPresent(cap -> {
-            HomeEvent event = cap.getLastDeviceEvent(handler.getId(), moduleType.apiName);
+            HomeEvent event = cap.getDeviceLastEvent(handler.getId(), moduleType.apiName);
             if (event != null) {
                 result.add(event);
                 result.addAll(event.getSubevents());
index 96b6174fa7405152b04746d74e107a45ab9ad605..1bc19c8e974758e6edafab907b24646286c15c84 100644 (file)
@@ -162,24 +162,24 @@ class SecurityCapability extends RestCapability<SecurityApi> {
         return event;
     }
 
-    public @Nullable HomeEvent getLastDeviceEvent(String cameraId, String deviceType) {
-        HomeEvent event = eventBuffer.get(cameraId);
+    public @Nullable HomeEvent getDeviceLastEvent(String moduleId, String deviceType) {
+        HomeEvent event = eventBuffer.get(moduleId);
         if (event == null) {
-            Collection<HomeEvent> events = requestDeviceEvents(cameraId, deviceType);
+            Collection<HomeEvent> events = requestDeviceEvents(moduleId, deviceType);
             if (!events.isEmpty()) {
                 event = events.iterator().next();
-                eventBuffer.put(cameraId, event);
+                eventBuffer.put(moduleId, event);
             }
         }
         return event;
     }
 
-    private Collection<HomeEvent> requestDeviceEvents(String cameraId, String deviceType) {
+    private Collection<HomeEvent> requestDeviceEvents(String moduleId, String deviceType) {
         return getApi().map(api -> {
             try {
-                return api.getDeviceEvents(handler.getId(), cameraId, deviceType);
+                return api.getDeviceEvents(handler.getId(), moduleId, deviceType);
             } catch (NetatmoException e) {
-                logger.warn("Error retrieving last events of camera '{}' : {}", cameraId, e.getMessage());
+                logger.warn("Error retrieving last events of camera '{}' : {}", moduleId, e.getMessage());
                 return null;
             }
         }).orElse(List.of());
diff --git a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/capability/SmokeCapability.java b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/capability/SmokeCapability.java
deleted file mode 100644 (file)
index 904a1cf..0000000
+++ /dev/null
@@ -1,50 +0,0 @@
-/**
- * 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.netatmo.internal.handler.capability;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.openhab.binding.netatmo.internal.api.dto.HomeEvent;
-import org.openhab.binding.netatmo.internal.api.dto.NAObject;
-import org.openhab.binding.netatmo.internal.handler.CommonInterface;
-import org.openhab.binding.netatmo.internal.handler.channelhelper.ChannelHelper;
-import org.openhab.binding.netatmo.internal.providers.NetatmoDescriptionProvider;
-
-/**
- * {@link SmokeCapability} gives the ability to handle Smoke detector specifics
- *
- * @author Gaël L'hopital - Initial contribution
- *
- */
-@NonNullByDefault
-public class SmokeCapability extends HomeSecurityThingCapability {
-
-    public SmokeCapability(CommonInterface handler, NetatmoDescriptionProvider descriptionProvider,
-            List<ChannelHelper> channelHelpers) {
-        super(handler, descriptionProvider, channelHelpers);
-    }
-
-    @Override
-    public List<NAObject> updateReadings() {
-        List<NAObject> result = new ArrayList<>();
-        securityCapability.ifPresent(cap -> {
-            HomeEvent event = cap.getLastDeviceEvent(handler.getId(), moduleType.apiName);
-            if (event != null) {
-                result.add(event);
-            }
-        });
-        return result;
-    }
-}
index 4f2f8042c3afb3e95a6bacba01ff0e3b53b461a6..aa6e856286de425c8d49268c1fc2805d6e9b6d45 100644 (file)
@@ -18,6 +18,9 @@ channel-group-type.netatmo.energy.label = Home Energy
 channel-group-type.netatmo.energy.channel.end.label = Mode End
 channel-group-type.netatmo.energy.channel.end.description = End time of the currently applied setpoint.
 channel-group-type.netatmo.humidity.label = Humidity
+channel-group-type.netatmo.last-event-alarm.label = Last Event
+channel-group-type.netatmo.last-event-alarm.channel.time.label = Event Timestamp
+channel-group-type.netatmo.last-event-alarm.channel.time.description = Moment when event occurred.
 channel-group-type.netatmo.last-event-doorbell.label = Last Event
 channel-group-type.netatmo.last-event-doorbell.channel.local-video-url.label = Video Local URL
 channel-group-type.netatmo.last-event-doorbell.channel.local-video-url.description = Local URL of the event recording.
@@ -31,9 +34,6 @@ channel-group-type.netatmo.last-event-person.channel.snapshot.description = Pict
 channel-group-type.netatmo.last-event-person.channel.snapshot-url.description = URL for the picture of the last event for this person.
 channel-group-type.netatmo.last-event-person.channel.time.label = Person Timestamp
 channel-group-type.netatmo.last-event-person.channel.time.description = Moment of the last event for this person.
-channel-group-type.netatmo.last-event-smoke.label = Last Event
-channel-group-type.netatmo.last-event-smoke.channel.time.label = Event Timestamp
-channel-group-type.netatmo.last-event-smoke.channel.time.description = Moment when event occurred.
 channel-group-type.netatmo.last-event.label = Last Event
 channel-group-type.netatmo.last-event.channel.local-video-url.label = Video Local URL
 channel-group-type.netatmo.last-event.channel.local-video-url.description = Local URL of the event recording.
@@ -174,8 +174,8 @@ channel-type.netatmo.event-subtype.state.option.MOVEMENT_VEHICLE = Car seen
 channel-type.netatmo.event-subtype.state.option.MOVEMENT_ANIMAL = Animal seen
 channel-type.netatmo.event-subtype.state.option.SOUND_TEST_OK = Alarm test successful
 channel-type.netatmo.event-subtype.state.option.SOUND_TEST_ERROR = Alarm test failed
-channel-type.netatmo.event-subtype.state.option.DETECTOR_READY = Smoke detector installed
-channel-type.netatmo.event-subtype.state.option.DETECTOR_TAMPERED = Smoke detector tampered
+channel-type.netatmo.event-subtype.state.option.DETECTOR_READY = Detector installed
+channel-type.netatmo.event-subtype.state.option.DETECTOR_TAMPERED = Detector tampered
 channel-type.netatmo.event-subtype.state.option.DETECTION_CHAMBER_CLEAN = Detection chamber clean
 channel-type.netatmo.event-subtype.state.option.DETECTION_CHAMBER_DIRTY = Detection chamber dusty
 channel-type.netatmo.event-subtype.state.option.BATTERY_LOW = Battery low
@@ -184,6 +184,9 @@ channel-type.netatmo.event-subtype.state.option.SMOKE_CLEARED = Smoke cleared
 channel-type.netatmo.event-subtype.state.option.SMOKE_DETECTED = Smoke detected
 channel-type.netatmo.event-subtype.state.option.WIFI_STATUS_OK = Wi-Fi status ok
 channel-type.netatmo.event-subtype.state.option.WIFI_STATUS_ERROR = Wi-Fi status error
+channel-type.netatmo.event-subtype.state.option.CO_OK = Carbon Monoxide OK
+channel-type.netatmo.event-subtype.state.option.CO_PRE_ALARM = Carbon Monoxide Pre-alarm
+channel-type.netatmo.event-subtype.state.option.CO_ALARM = Carbon Monoxide alarrm
 channel-type.netatmo.event-type.label = Event Type
 channel-type.netatmo.event-type.description = Description of the event.
 channel-type.netatmo.event-type.state.option.PERSON = Face detected
@@ -212,12 +215,13 @@ channel-type.netatmo.event-type.state.option.RTC = Button pressed
 channel-type.netatmo.event-type.state.option.MISSED_CALL = Call has not been answered by anyone
 channel-type.netatmo.event-type.state.option.HUSH = Smoke detector status
 channel-type.netatmo.event-type.state.option.SMOKE = Smoke detection
-channel-type.netatmo.event-type.state.option.TAMPERED = Smoke Detector tamper
+channel-type.netatmo.event-type.state.option.TAMPERED = Detector tamper
 channel-type.netatmo.event-type.state.option.WIFI_STATUS = Wifi status
 channel-type.netatmo.event-type.state.option.BATTERY_STATUS = Battery status
 channel-type.netatmo.event-type.state.option.DETECTION_CHAMBER_STATUS = Detection chamber status
 channel-type.netatmo.event-type.state.option.SOUND_TEST = Sound test
 channel-type.netatmo.event-type.state.option.NEW_DEVICE = A device has been added
+channel-type.netatmo.event-type.state.option.CO_DETECTED = Carbon Monoxide detection
 channel-type.netatmo.floodlight-mode.label = Floodlight
 channel-type.netatmo.floodlight-mode.description = State of the floodlight (On/Off/Auto)
 channel-type.netatmo.floodlight-mode.state.option.ON = On
@@ -378,6 +382,8 @@ extensible-channel-type.timestamp.pattern = %1$tA, %1$td.%1$tm. %1$tH:%1$tM
 
 thing-type.netatmo.account.label = Netatmo Account
 thing-type.netatmo.account.description = This bridge represents an account, gateway to Netatmo API.
+thing-type.netatmo.co-detector.label = Carbon Monoxide Alarm
+thing-type.netatmo.co-detector.description = The Netatmo Smart Carbon Monoxide Alarm device.
 thing-type.netatmo.doorbell.label = Smart Video Doorbell
 thing-type.netatmo.doorbell.description = The Netatmo Smart Video Doorbell device.
 thing-type.netatmo.tag.label = Smart Door Sensor
index 97efdce8269c50d91d10c1a8918ad7ae45b00c73..2a0f6a5bbaa5b10e08cfcac97f990800620a9b5b 100644 (file)
                                <option value="MISSED_CALL">Call has not been answered by anyone</option>
                                <option value="HUSH">Smoke detector status</option>
                                <option value="SMOKE">Smoke detection</option>
-                               <option value="TAMPERED">Smoke Detector tamper</option>
+                               <option value="TAMPERED">Detector tamper</option>
                                <option value="WIFI_STATUS">Wifi status</option>
                                <option value="BATTERY_STATUS">Battery status</option>
                                <option value="DETECTION_CHAMBER_STATUS">Detection chamber status</option>
                                <option value="SOUND_TEST">Sound test</option>
                                <option value="NEW_DEVICE">A device has been added</option>
+                               <option value="CO_DETECTED">Carbon Monoxide detection</option>
                        </options>
                </state>
        </channel-type>
                                <option value="MOVEMENT_ANIMAL">Animal seen</option>
                                <option value="SOUND_TEST_OK">Alarm test successful</option>
                                <option value="SOUND_TEST_ERROR">Alarm test failed</option>
-                               <option value="DETECTOR_READY">Smoke detector installed</option>
-                               <option value="DETECTOR_TAMPERED">Smoke detector tampered</option>
+                               <option value="DETECTOR_READY">Detector installed</option>
+                               <option value="DETECTOR_TAMPERED">Detector tampered</option>
                                <option value="DETECTION_CHAMBER_CLEAN">Detection chamber clean</option>
                                <option value="DETECTION_CHAMBER_DIRTY">Detection chamber dusty</option>
                                <option value="BATTERY_LOW">Battery low</option>
                                <option value="SMOKE_DETECTED">Smoke detected</option>
                                <option value="WIFI_STATUS_OK">Wi-Fi status ok</option>
                                <option value="WIFI_STATUS_ERROR">Wi-Fi status error</option>
+                               <option value="CO_OK">Carbon Monoxide OK</option>
+                               <option value="CO_PRE_ALARM">Carbon Monoxide Pre-alarm</option>
+                               <option value="CO_ALARM">Carbon Monoxide alarrm</option>
                        </options>
                </state>
        </channel-type>
index cf410d75a8a89882263f60f1d9f6661d8c8e578a..fa817e532157e0222dc401b71eb402bf855f45b2 100644 (file)
                </channels>
        </channel-group-type>
 
-       <channel-group-type id="last-event-smoke">
+       <channel-group-type id="last-event-alarm">
                <label>Last Event</label>
                <channels>
                        <channel id="type" typeId="event-type"/>