]> git.basschouten.com Git - openhab-addons.git/commitdiff
[netatmo] Add siren device (#12805)
authorGaël L'hopital <gael@lhopital.org>
Sat, 28 May 2022 09:26:30 +0000 (11:26 +0200)
committerGitHub <noreply@github.com>
Sat, 28 May 2022 09:26:30 +0000 (11:26 +0200)
* Starting Siren addition

Signed-off-by: clinique <gael@lhopital.org>
19 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/api/AuthenticationApi.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/api/dto/HomeDataModule.java
bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/deserialization/ModuleTypeDeserializer.java [new file with mode: 0644]
bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/deserialization/NADeserializer.java
bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/discovery/NetatmoDiscoveryService.java
bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/CommonInterface.java
bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/capability/Capability.java
bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/capability/EnergyCapability.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/channelhelper/SirenChannelHelper.java [new file with mode: 0644]
bundles/org.openhab.binding.netatmo/src/main/resources/OH-INF/i18n/netatmo.properties
bundles/org.openhab.binding.netatmo/src/main/resources/OH-INF/i18n/netatmo_it.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/energy.xml
bundles/org.openhab.binding.netatmo/src/main/resources/OH-INF/thing/security.xml

index 32925540656ce84c57dec57cccac85ccc1ae20e9..58d5a6f132b1cb7082c86fae248ea4d38605e3f0 100644 (file)
@@ -5,7 +5,9 @@ The Netatmo binding integrates the following Netatmo products:
 - *Personal Weather Station*. Reports temperature, humidity, air pressure, carbon dioxide concentration in the air, as well as the ambient noise level.
 - *Thermostat*. Reports ambient temperature, allow to check target temperature, consult and change furnace heating status.
 - *Indoor Camera / Welcome*. Reports last event and persons at home, consult picture and video from event/camera.
+- *Siren*
 - *Outdoor Camera / Presence*. Reports last event, consult picture and video from event/camera.
+- *Doorbell* 
 
 See https://www.netatmo.com/ for details on their product.
 
@@ -505,7 +507,7 @@ Warnings:
 
 (*) This channel is configurable : low, poor, high.
 
-**Supported channels for the Welcome Doorbell thing:**
+**Supported channels for the Doorbell thing:**
 
 | Channel Group | Channel ID        | Item Type    | Read/Write | Description                                                                                                                                 |
 |---------------|-------------------|--------------|------------|---------------------------------------------------------------------------------------------------------------------------------------------|
@@ -532,6 +534,18 @@ Warnings:
 
 Note: live feeds either locally or via VPN are not available in Netatmo API.
 
+**Supported channels for the Siren thing:**
+
+| Channel Group | Channel ID        | Item Type    | Read/Write | Description                                          |
+|---------------|-------------------|--------------|------------|------------------------------------------------------|
+| siren         | status            | String       | Read-only  | Status of the siren, if silent or emitting an alarm  |
+| siren         | monitoring        | Switch       | Read-only  | State of the siren device                            |
+| signal        | strength          | Number       | Read-only  | Signal strength (0 for no signal, 1 for weak...)     |
+| signal        | value             | Number:Power | Read-only  | Signal strength in dBm                               |
+| timestamp     | last-seen         | DateTime     | Read-only  | Last time the module reported its presence           |
+| battery       | value             | Number       | Read-only  | Battery level                                        |
+| battery       | low-battery       | Switch       | Read-only  | Low battery                                          |
+
 
 ### Welcome Person
 
index c4a3d585fc4f078011f6263fe415fd5c0e24b778..9878a32292789a8d78d6954fb2f6f68dee2c4ba9 100644 (file)
@@ -53,6 +53,7 @@ public class NetatmoBindingConstants {
     public static final String GROUP_CAM_STATUS = "status";
     public static final String GROUP_CAM_LIVE = "live";
     public static final String GROUP_PRESENCE = "presence";
+    public static final String GROUP_SIREN = "siren";
     public static final String GROUP_PERSON = "person";
     public static final String GROUP_PROPERTIES = "properties";
     public static final String GROUP_SETPOINT = "setpoint";
@@ -105,6 +106,7 @@ public class NetatmoBindingConstants {
     public static final String CHANNEL_SUM_RAIN1 = "sum-1";
     public static final String CHANNEL_SUM_RAIN24 = "sum-24";
     public static final String CHANNEL_WIND_ANGLE = "angle";
+    public static final String CHANNEL_STATUS = GROUP_CAM_STATUS;
     public static final String CHANNEL_WIND_STRENGTH = "strength";
     public static final String CHANNEL_MAX_WIND_STRENGTH = "max-strength";
     public static final String CHANNEL_DATE_MAX_WIND_STRENGTH = "max-strength-date";
index 926ce416855351f6e4480276d0b22863a69e865d..a95b335cc35bb799bd0cf5145eb92ab34a71486f 100644 (file)
@@ -43,7 +43,6 @@ import org.slf4j.LoggerFactory;
  */
 @NonNullByDefault
 public class AuthenticationApi extends RestManager {
-    private static final String ALL_SCOPES = FeatureArea.toScopeString(FeatureArea.AS_SET);
     private static final UriBuilder OAUTH_BUILDER = getApiBaseBuilder().path(PATH_OAUTH);
     private static final UriBuilder AUTH_BUILDER = OAUTH_BUILDER.clone().path(SUB_PATH_AUTHORIZE);
     private static final URI TOKEN_URI = OAUTH_BUILDER.clone().path(SUB_PATH_TOKEN).build();
@@ -62,7 +61,7 @@ public class AuthenticationApi extends RestManager {
     public String authorize(ApiHandlerConfiguration credentials, @Nullable String code, @Nullable String redirectUri)
             throws NetatmoException {
         if (!(credentials.clientId.isBlank() || credentials.clientSecret.isBlank())) {
-            Map<String, String> params = new HashMap<>(Map.of(SCOPE, ALL_SCOPES));
+            Map<String, String> params = new HashMap<>(Map.of(SCOPE, FeatureArea.ALL_SCOPES));
             String refreshToken = credentials.refreshToken;
             if (!refreshToken.isBlank()) {
                 params.put(REFRESH_TOKEN, refreshToken);
@@ -118,7 +117,7 @@ public class AuthenticationApi extends RestManager {
     }
 
     public static UriBuilder getAuthorizationBuilder(String clientId) {
-        return AUTH_BUILDER.clone().queryParam(CLIENT_ID, clientId).queryParam(SCOPE, ALL_SCOPES).queryParam(STATE,
-                clientId);
+        return AUTH_BUILDER.clone().queryParam(CLIENT_ID, clientId).queryParam(SCOPE, FeatureArea.ALL_SCOPES)
+                .queryParam(STATE, clientId);
     }
 }
index 5a0f9acb8ea304a9191e5925940a65d906712f33..34adc622f6fa497f721c46918358f445b2ce0c05 100644 (file)
@@ -59,6 +59,7 @@ import org.openhab.binding.netatmo.internal.handler.channelhelper.RainChannelHel
 import org.openhab.binding.netatmo.internal.handler.channelhelper.RoomChannelHelper;
 import org.openhab.binding.netatmo.internal.handler.channelhelper.SetpointChannelHelper;
 import org.openhab.binding.netatmo.internal.handler.channelhelper.SignalChannelHelper;
+import org.openhab.binding.netatmo.internal.handler.channelhelper.SirenChannelHelper;
 import org.openhab.binding.netatmo.internal.handler.channelhelper.TemperatureChannelHelper;
 import org.openhab.binding.netatmo.internal.handler.channelhelper.TemperatureExtChannelHelper;
 import org.openhab.binding.netatmo.internal.handler.channelhelper.TemperatureOutChannelHelper;
@@ -68,8 +69,6 @@ import org.openhab.binding.netatmo.internal.handler.channelhelper.TimestampExtCh
 import org.openhab.binding.netatmo.internal.handler.channelhelper.WindChannelHelper;
 import org.openhab.core.thing.ThingTypeUID;
 
-import com.google.gson.annotations.SerializedName;
-
 /**
  * This enum all handled Netatmo modules and devices along with their capabilities
  *
@@ -79,75 +78,76 @@ import com.google.gson.annotations.SerializedName;
 public enum ModuleType {
     UNKNOWN(FeatureArea.NONE, "", null, List.of(), List.of()),
     ACCOUNT(FeatureArea.NONE, "", null, List.of(), List.of()),
-    @SerializedName("NAHome")
+
     HOME(FeatureArea.NONE, "NAHome", ACCOUNT,
             List.of(DeviceCapability.class, EventCapability.class, HomeCapability.class, ChannelHelperCapability.class),
             List.of(HomeSecurityChannelHelper.class, HomeEnergyChannelHelper.class)),
-    @SerializedName("NAPerson")
+
     PERSON(FeatureArea.SECURITY, "NAPerson", HOME,
             List.of(EventCapability.class, PersonCapability.class, ChannelHelperCapability.class),
             List.of(PersonChannelHelper.class, EventPersonChannelHelper.class)),
-    @SerializedName("NACamera")
+
     WELCOME(FeatureArea.SECURITY, "NACamera", HOME,
             List.of(EventCapability.class, CameraCapability.class, ChannelHelperCapability.class),
             List.of(CameraChannelHelper.class, SignalChannelHelper.class, EventChannelHelper.class)),
-    @SerializedName("NOC")
+
+    SIREN(FeatureArea.SECURITY, "NIS", WELCOME, List.of(ChannelHelperCapability.class),
+            List.of(SirenChannelHelper.class, BatteryChannelHelper.class, TimestampChannelHelper.class,
+                    SignalChannelHelper.class)),
+
     PRESENCE(FeatureArea.SECURITY, "NOC", HOME,
             List.of(EventCapability.class, PresenceCapability.class, ChannelHelperCapability.class),
             List.of(PresenceChannelHelper.class, SignalChannelHelper.class, EventChannelHelper.class)),
-    @SerializedName("NIS")
-    SIREN(FeatureArea.SECURITY, "NIS", HOME, List.of(ChannelHelperCapability.class),
-            List.of(BatteryChannelHelper.class, TimestampChannelHelper.class, SignalChannelHelper.class)),
-    @SerializedName("NDB")
+
     DOORBELL(FeatureArea.SECURITY, "NDB", HOME,
             List.of(EventCapability.class, CameraCapability.class, ChannelHelperCapability.class),
             List.of(DoorbellChannelHelper.class, SignalChannelHelper.class, EventDoorbellChannelHelper.class)),
-    @SerializedName("NAMain")
+
     WEATHER_STATION(FeatureArea.WEATHER, "NAMain", ACCOUNT,
             List.of(DeviceCapability.class, WeatherCapability.class, MeasureCapability.class,
                     ChannelHelperCapability.class),
             List.of(PressureExtChannelHelper.class, NoiseChannelHelper.class, HumidityChannelHelper.class,
                     TemperatureExtChannelHelper.class, AirQualityChannelHelper.class, LocationChannelHelper.class,
                     TimestampExtChannelHelper.class, MeasuresChannelHelper.class, SignalChannelHelper.class)),
-    @SerializedName("NAModule1")
+
     OUTDOOR(FeatureArea.WEATHER, "NAModule1", WEATHER_STATION,
             List.of(MeasureCapability.class, ChannelHelperCapability.class),
             List.of(HumidityChannelHelper.class, TemperatureOutChannelHelper.class, BatteryChannelHelper.class,
                     MeasuresChannelHelper.class, TimestampExtChannelHelper.class, SignalChannelHelper.class)),
-    @SerializedName("NAModule2")
+
     WIND(FeatureArea.WEATHER, "NAModule2", WEATHER_STATION, List.of(ChannelHelperCapability.class),
             List.of(WindChannelHelper.class, BatteryChannelHelper.class, TimestampExtChannelHelper.class,
                     SignalChannelHelper.class)),
-    @SerializedName("NAModule3")
+
     RAIN(FeatureArea.WEATHER, "NAModule3", WEATHER_STATION,
             List.of(MeasureCapability.class, ChannelHelperCapability.class),
             List.of(RainChannelHelper.class, BatteryChannelHelper.class, MeasuresChannelHelper.class,
                     TimestampExtChannelHelper.class, SignalChannelHelper.class)),
-    @SerializedName("NAModule4")
+
     INDOOR(FeatureArea.WEATHER, "NAModule4", WEATHER_STATION,
             List.of(MeasureCapability.class, ChannelHelperCapability.class),
             List.of(HumidityChannelHelper.class, TemperatureExtChannelHelper.class, AirQualityChannelHelper.class,
                     BatteryChannelHelper.class, MeasuresChannelHelper.class, TimestampExtChannelHelper.class,
                     SignalChannelHelper.class)),
-    @SerializedName("NHC")
+
     HOME_COACH(FeatureArea.AIR_CARE, "NHC", ACCOUNT,
             List.of(DeviceCapability.class, AirCareCapability.class, MeasureCapability.class,
                     ChannelHelperCapability.class),
             List.of(NoiseChannelHelper.class, HumidityChannelHelper.class, AirQualityExtChannelHelper.class,
                     TemperatureChannelHelper.class, PressureChannelHelper.class, TimestampExtChannelHelper.class,
                     SignalChannelHelper.class, MeasuresChannelHelper.class, LocationChannelHelper.class)),
-    @SerializedName("NAPlug")
+
     PLUG(FeatureArea.ENERGY, "NAPlug", HOME, List.of(ChannelHelperCapability.class),
             List.of(SignalChannelHelper.class)),
-    @SerializedName("NATherm1")
-    THERMOSTAT(FeatureArea.ENERGY, "NATherm1", HOME, List.of(ChannelHelperCapability.class),
+
+    VALVE(FeatureArea.ENERGY, "NRV", PLUG, List.of(ChannelHelperCapability.class),
+            List.of(BatteryExtChannelHelper.class, SignalChannelHelper.class)),
+
+    THERMOSTAT(FeatureArea.ENERGY, "NATherm1", PLUG, List.of(ChannelHelperCapability.class),
             List.of(Therm1ChannelHelper.class, BatteryExtChannelHelper.class, SignalChannelHelper.class)),
-    @SerializedName("NARoom")
+
     ROOM(FeatureArea.ENERGY, "NARoom", HOME, List.of(RoomCapability.class, ChannelHelperCapability.class),
-            List.of(RoomChannelHelper.class, SetpointChannelHelper.class)),
-    @SerializedName("NRV")
-    VALVE(FeatureArea.ENERGY, "NRV", HOME, List.of(ChannelHelperCapability.class),
-            List.of(BatteryExtChannelHelper.class, SignalChannelHelper.class));
+            List.of(RoomChannelHelper.class, SetpointChannelHelper.class));
 
     public static final EnumSet<ModuleType> AS_SET = EnumSet.allOf(ModuleType.class);
 
index 80e8ef6cc86c1f434675d5e90422dd301a92f983..fdd7d2340b85906f6bd7c4c8000be972f992a099 100644 (file)
@@ -17,6 +17,7 @@ import static org.openhab.core.library.CoreItemFactory.*;
 import static org.openhab.core.library.unit.MetricPrefix.*;
 
 import java.net.URI;
+import java.util.Arrays;
 import java.util.EnumSet;
 import java.util.HashMap;
 import java.util.Map;
@@ -190,36 +191,35 @@ public class NetatmoConstants {
         UNKNOWN;
     }
 
-    private static final Set<Scope> SMOKE = Set.of(Scope.READ_SMOKEDETECTOR);
-    private static final Set<Scope> WELCOME = Set.of(Scope.READ_CAMERA, Scope.WRITE_CAMERA, Scope.ACCESS_CAMERA);
-    private static final Set<Scope> DOORBELL = Set.of(Scope.READ_DOORBELL, Scope.WRITE_DOORBELL, Scope.ACCESS_DOORBELL);
-    private static final Set<Scope> PRESENCE = Set.of(Scope.READ_PRESENCE, Scope.WRITE_PRESENCE, Scope.ACCESS_PRESENCE);
-
-    // Radio signal quality thresholds
-    static final int[] WIFI_SIGNAL_LEVELS = new int[] { 99, 84, 69, 54 }; // Resp : bad, average, good, full
-    static final int[] RADIO_SIGNAL_LEVELS = new int[] { 90, 80, 70, 60 }; // Resp : low, medium, high, full
+    private static final Scope[] SMOKE_SCOPES = { Scope.READ_SMOKEDETECTOR };
+    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 };
+    private static final Scope[] WELCOME_SCOPES = { Scope.READ_CAMERA, Scope.WRITE_CAMERA, Scope.ACCESS_CAMERA };
+    private static final Scope[] DOORBELL_SCOPES = { Scope.READ_DOORBELL, Scope.WRITE_DOORBELL, Scope.ACCESS_DOORBELL };
+    private static final Scope[] PRESENCE_SCOPES = { Scope.READ_PRESENCE, Scope.WRITE_PRESENCE, Scope.ACCESS_PRESENCE };
 
     public static enum FeatureArea {
-        AIR_CARE(Scope.READ_HOMECOACH),
-        WEATHER(Scope.READ_STATION),
-        ENERGY(Scope.READ_THERMOSTAT, Scope.WRITE_THERMOSTAT),
-        SECURITY(Stream.of(WELCOME, PRESENCE, SMOKE, DOORBELL).flatMap(Set::stream).toArray(Scope[]::new)),
+        AIR_CARE(AIR_CARE_SCOPES),
+        WEATHER(WEATHER_SCOPES),
+        ENERGY(THERMOSTAT_SCOPES),
+        SECURITY(WELCOME_SCOPES, PRESENCE_SCOPES, SMOKE_SCOPES, DOORBELL_SCOPES),
         NONE();
 
-        public static final Set<FeatureArea> AS_SET = EnumSet.allOf(FeatureArea.class);
-
-        public static String toScopeString(Set<FeatureArea> featureSet) {
-            return featureSet.stream().map(fa -> fa.scopes).flatMap(Set::stream).map(s -> s.name().toLowerCase())
-                    .collect(Collectors.joining(" "));
-        }
+        public static String ALL_SCOPES = EnumSet.allOf(FeatureArea.class).stream().map(fa -> fa.scopes)
+                .flatMap(Set::stream).map(s -> s.name().toLowerCase()).collect(Collectors.joining(" "));
 
         public final Set<Scope> scopes;
 
-        FeatureArea(Scope... scopes) {
-            this.scopes = Set.of(scopes);
+        FeatureArea(Scope[]... scopeArrays) {
+            this.scopes = Stream.of(scopeArrays).flatMap(Arrays::stream).collect(Collectors.toSet());
         }
     }
 
+    // Radio signal quality thresholds
+    static final int[] WIFI_SIGNAL_LEVELS = new int[] { 99, 84, 69, 54 }; // Resp : bad, average, good, full
+    static final int[] RADIO_SIGNAL_LEVELS = new int[] { 90, 80, 70, 60 }; // Resp : low, medium, high, full
+
     // Thermostat definitions
     public static enum SetpointMode {
         @SerializedName("program")
index da2c91b630d397996c7ec9b9ec279587cfbfba02..5f0ac3b871d1530ab1e432e97846fe2d461212e2 100644 (file)
@@ -43,4 +43,9 @@ public class HomeDataModule extends NAThing implements NAModule {
     public List<String> getModuleBridged() {
         return moduleBridged;
     }
+
+    @Override
+    public boolean isIgnoredForThingUpdate() {
+        return true;
+    }
 }
diff --git a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/deserialization/ModuleTypeDeserializer.java b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/deserialization/ModuleTypeDeserializer.java
new file mode 100644 (file)
index 0000000..9546889
--- /dev/null
@@ -0,0 +1,39 @@
+/**
+ * Copyright (c) 2010-2022 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.deserialization;
+
+import java.lang.reflect.Type;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.netatmo.internal.api.data.ModuleType;
+
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+
+/**
+ * Specialized deserializer for ModuleType class
+ *
+ * @author Gaël L'hopital - Initial contribution
+ */
+@NonNullByDefault
+public class ModuleTypeDeserializer implements JsonDeserializer<ModuleType> {
+
+    @Override
+    public @Nullable ModuleType deserialize(JsonElement json, Type clazz, JsonDeserializationContext context) {
+        String string = json.getAsString();
+        return ModuleType.AS_SET.stream().filter(mt -> mt.apiName.equalsIgnoreCase(string)).findFirst()
+                .orElse(ModuleType.UNKNOWN);
+    }
+}
index 1976cf7f88cf24ad5820cee63852c63a543dca97..f3c45a2ab08e9fce17ba0c98c73eee42b85ee537 100644 (file)
@@ -18,6 +18,7 @@ import java.time.ZonedDateTime;
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
 import org.openhab.binding.netatmo.internal.api.NetatmoException;
+import org.openhab.binding.netatmo.internal.api.data.ModuleType;
 import org.openhab.core.i18n.TimeZoneProvider;
 import org.openhab.core.library.types.OnOffType;
 import org.openhab.core.library.types.OpenClosedType;
@@ -47,6 +48,7 @@ public class NADeserializer {
                 .registerTypeAdapterFactory(new StrictEnumTypeAdapterFactory())
                 .registerTypeAdapter(NAObjectMap.class, new NAObjectMapDeserializer())
                 .registerTypeAdapter(NAPushType.class, new NAPushTypeDeserializer())
+                .registerTypeAdapter(ModuleType.class, new ModuleTypeDeserializer())
                 .registerTypeAdapter(ZonedDateTime.class,
                         (JsonDeserializer<ZonedDateTime>) (json, type, jsonDeserializationContext) -> {
                             long netatmoTS = json.getAsJsonPrimitive().getAsLong();
index 23bcfdeacd34f4d107e1a3c547c06f81b5184639..a518971a41e96a8a5c46ef8a0b8c483a230d9974 100644 (file)
@@ -12,6 +12,8 @@
  */
 package org.openhab.binding.netatmo.internal.discovery;
 
+import java.util.HashMap;
+import java.util.Map;
 import java.util.Set;
 import java.util.stream.Collectors;
 
@@ -47,7 +49,7 @@ import org.slf4j.LoggerFactory;
 @NonNullByDefault
 public class NetatmoDiscoveryService extends AbstractDiscoveryService implements ThingHandlerService, DiscoveryService {
     private static final Set<ModuleType> SKIPPED_TYPES = Set.of(ModuleType.UNKNOWN, ModuleType.ACCOUNT);
-    private static final int DISCOVER_TIMEOUT_SECONDS = 5;
+    private static final int DISCOVER_TIMEOUT_SECONDS = 3;
     private final Logger logger = LoggerFactory.getLogger(NetatmoDiscoveryService.class);
     private @Nullable ApiBridgeHandler handler;
 
@@ -60,13 +62,13 @@ public class NetatmoDiscoveryService extends AbstractDiscoveryService implements
     public void startScan() {
         ApiBridgeHandler localHandler = handler;
         if (localHandler != null) {
-            ThingUID apiBridgeUID = localHandler.getThing().getUID();
+            ThingUID accountUID = localHandler.getThing().getUID();
             try {
                 AircareApi airCareApi = localHandler.getRestManager(AircareApi.class);
                 if (airCareApi != null) { // Search Healthy Home Coaches
                     ListBodyResponse<NAMain> body = airCareApi.getHomeCoachData(null).getBody();
                     if (body != null) {
-                        body.getElements().stream().forEach(homeCoach -> createThing(homeCoach, apiBridgeUID));
+                        body.getElements().stream().forEach(homeCoach -> createThing(homeCoach, accountUID));
                     }
                 }
                 if (localHandler.getReadFriends()) {
@@ -74,31 +76,34 @@ public class NetatmoDiscoveryService extends AbstractDiscoveryService implements
                     if (weatherApi != null) { // Search favorite stations
                         weatherApi.getFavoriteAndGuestStationsData().stream().filter(NAMain::isReadOnly)
                                 .forEach(station -> {
-                                    ThingUID bridgeUID = createThing(station, apiBridgeUID);
+                                    ThingUID bridgeUID = createThing(station, accountUID);
                                     station.getModules().values().stream()
                                             .forEach(module -> createThing(module, bridgeUID));
                                 });
                     }
                 }
                 HomeApi homeApi = localHandler.getRestManager(HomeApi.class);
-                if (homeApi != null) { // Search all the rest
+                if (homeApi != null) { // Search those who depend from a home
                     homeApi.getHomesData(null, null).stream().filter(h -> !h.getFeatures().isEmpty()).forEach(home -> {
-                        ThingUID homeUID = createThing(home, apiBridgeUID);
+                        ThingUID homeUID = createThing(home, accountUID);
+
                         home.getKnownPersons().forEach(person -> createThing(person, homeUID));
-                        home.getModules().values().stream().forEach(device -> {
-                            ModuleType deviceType = device.getType();
-                            String deviceBridge = device.getBridge();
-                            ThingUID bridgeUID = deviceBridge != null && deviceType.getBridge() != ModuleType.HOME
-                                    ? findThingUID(deviceType.getBridge(), deviceBridge, apiBridgeUID)
-                                    : deviceType.getBridge() == ModuleType.HOME ? homeUID : apiBridgeUID;
-                            createThing(device, bridgeUID);
-                        });
+
+                        Map<String, ThingUID> bridgesUids = new HashMap<>();
+
                         home.getRooms().values().stream().forEach(room -> {
                             room.getModuleIds().stream().map(id -> home.getModules().get(id))
                                     .map(m -> m != null ? m.getType().feature : FeatureArea.NONE)
                                     .filter(f -> FeatureArea.ENERGY.equals(f)).findAny()
-                                    .ifPresent(f -> createThing(room, homeUID));
+                                    .ifPresent(f -> bridgesUids.put(room.getId(), createThing(room, homeUID)));
                         });
+
+                        // Creating modules that have no bridge first
+                        home.getModules().values().stream().filter(module -> module.getBridge() == null)
+                                .forEach(device -> bridgesUids.put(device.getId(), createThing(device, homeUID)));
+                        // Then the others
+                        home.getModules().values().stream().filter(module -> module.getBridge() != null).forEach(
+                                device -> createThing(device, bridgesUids.getOrDefault(device.getBridge(), homeUID)));
                     });
                 }
             } catch (NetatmoException e) {
@@ -107,26 +112,19 @@ public class NetatmoDiscoveryService extends AbstractDiscoveryService implements
         }
     }
 
-    private ThingUID findThingUID(ModuleType thingType, String thingId, @Nullable ThingUID brigdeUID) {
-        for (ThingTypeUID supported : getSupportedThingTypes()) {
-            ThingTypeUID thingTypeUID = thingType.thingTypeUID;
-            if (supported.equals(thingTypeUID)) {
-                String id = thingId.replaceAll("[^a-zA-Z0-9_]", "");
-                return brigdeUID == null ? new ThingUID(supported, id) : new ThingUID(supported, brigdeUID, id);
-            }
-        }
-        throw new IllegalArgumentException("Unsupported device type discovered : " + thingType);
+    private ThingUID findThingUID(ModuleType thingType, String thingId, ThingUID bridgeUID) {
+        ThingTypeUID thingTypeUID = thingType.thingTypeUID;
+        return getSupportedThingTypes().stream().filter(supported -> supported.equals(thingTypeUID)).findFirst()
+                .map(supported -> new ThingUID(supported, bridgeUID, thingId.replaceAll("[^a-zA-Z0-9_]", "")))
+                .orElseThrow(() -> new IllegalArgumentException("Unsupported device type discovered : " + thingType));
     }
 
-    private ThingUID createThing(NAModule module, @Nullable ThingUID bridgeUID) {
+    private ThingUID createThing(NAModule module, ThingUID bridgeUID) {
         ThingUID moduleUID = findThingUID(module.getType(), module.getId(), bridgeUID);
         DiscoveryResultBuilder resultBuilder = DiscoveryResultBuilder.create(moduleUID)
                 .withProperty(NAThingConfiguration.ID, module.getId())
                 .withRepresentationProperty(NAThingConfiguration.ID)
-                .withLabel(module.getName() != null ? module.getName() : module.getId());
-        if (bridgeUID != null) {
-            resultBuilder.withBridge(bridgeUID);
-        }
+                .withLabel(module.getName() != null ? module.getName() : module.getId()).withBridge(bridgeUID);
         thingDiscovered(resultBuilder.build());
         return moduleUID;
     }
index d620431126fcfdf1be32bf95eb62273e326efab4..18eb8b3ffede22f23e312b6206142eca5852c83c 100644 (file)
@@ -25,6 +25,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
 import org.openhab.binding.netatmo.internal.api.data.ModuleType;
 import org.openhab.binding.netatmo.internal.api.dto.NAObject;
+import org.openhab.binding.netatmo.internal.api.dto.NAThing;
 import org.openhab.binding.netatmo.internal.config.NAThingConfiguration;
 import org.openhab.binding.netatmo.internal.handler.capability.Capability;
 import org.openhab.binding.netatmo.internal.handler.capability.CapabilityMap;
@@ -139,6 +140,14 @@ public interface CommonInterface {
     }
 
     default void setNewData(NAObject newData) {
+        if (newData instanceof NAThing) {
+            NAThing thingData = (NAThing) newData;
+            if (getId().equals(thingData.getBridge())) {
+                getActiveChildren().stream().filter(child -> child.getId().equals(thingData.getId())).findFirst()
+                        .ifPresent(child -> child.setNewData(thingData));
+                return;
+            }
+        }
         String finalReason = null;
         for (Capability cap : getCapabilities().values()) {
             String thingStatusReason = cap.setNewData(newData);
index a2ef48a640c77f7a2a9f934acda333d8ca4a9678..380c6f5f2497580c57d333e6ab6632b0b857fb4a 100644 (file)
@@ -94,8 +94,9 @@ public class Capability {
         properties = new HashMap<>(thing.getProperties());
         firstLaunch = properties.isEmpty();
         if (firstLaunch && !moduleType.isLogical()) {
+            String name = moduleType.apiName.isBlank() ? moduleType.name() : moduleType.apiName;
+            properties.put(PROPERTY_MODEL_ID, name);
             properties.put(PROPERTY_VENDOR, VENDOR);
-            properties.put(PROPERTY_MODEL_ID, moduleType.name());
         }
         statusReason = null;
     }
index 328eb26227dd74cd95f12c46649d53fbb645e6ea..3589f9d37949fe78998007ac033a69aad99ef416 100644 (file)
@@ -58,17 +58,21 @@ public class EnergyCapability extends RestCapability<EnergyApi> {
     protected void updateHomeData(HomeData homeData) {
         NAObjectMap<HomeDataRoom> rooms = homeData.getRooms();
         NAObjectMap<HomeDataModule> modules = homeData.getModules();
-        handler.getActiveChildren().forEach(handler -> {
-            HomeDataRoom roomData = rooms.get(handler.getId());
-            if (roomData != null) {
+        handler.getActiveChildren().forEach(childHandler -> {
+            String childId = childHandler.getId();
+            rooms.getOpt(childId).ifPresentOrElse(roomData -> {
                 roomData.setIgnoredForThingUpdate(true);
-                handler.setNewData(roomData);
-            }
-            HomeDataModule moduleData = modules.get(handler.getId());
-            if (moduleData != null) {
-                moduleData.setIgnoredForThingUpdate(true);
-                handler.setNewData(moduleData);
-            }
+                childHandler.setNewData(roomData);
+            }, () -> {
+                modules.getOpt(childId).ifPresent(childData -> {
+                    childData.setIgnoredForThingUpdate(true);
+                    childHandler.setNewData(childData);
+                });
+                modules.values().stream().filter(module -> childId.equals(module.getBridge()))
+                        .forEach(bridgedModule -> {
+                            childHandler.setNewData(bridgedModule);
+                        });
+            });
         });
         descriptionProvider.setStateOptions(new ChannelUID(thing.getUID(), GROUP_ENERGY, CHANNEL_PLANNING),
                 homeData.getThermSchedules().stream().map(p -> new StateOption(p.getId(), p.getName()))
@@ -80,15 +84,23 @@ public class EnergyCapability extends RestCapability<EnergyApi> {
     protected void updateHomeStatus(HomeStatus homeStatus) {
         NAObjectMap<Room> rooms = homeStatus.getRooms();
         NAObjectMap<HomeStatusModule> modules = homeStatus.getModules();
-        handler.getActiveChildren().forEach(handler -> {
-            Room roomData = rooms.get(handler.getId());
-            if (roomData != null) {
-                handler.setNewData(roomData);
-            }
-            HomeStatusModule data = modules.get(handler.getId());
-            if (data != null) {
-                handler.setNewData(data);
-            }
+        handler.getActiveChildren().forEach(childHandler -> {
+            String childId = childHandler.getId();
+            rooms.getOpt(childId).ifPresentOrElse(roomData -> childHandler.setNewData(roomData), () -> {
+                modules.getOpt(childId).ifPresentOrElse(childData -> {
+                    childHandler.setNewData(childData);
+                    modules.values().stream().filter(module -> childId.equals(module.getBridge()))
+                            .forEach(bridgedModule -> {
+                                childHandler.setNewData(bridgedModule);
+                            });
+
+                }, () -> {
+                    // This module is not present in the homestatus data, so it is considered as unreachable
+                    HomeStatusModule module = new HomeStatusModule();
+                    module.setReachable(false);
+                    childHandler.setNewData(module);
+                });
+            });
         });
     }
 
index c19757d4aec2e000b5686348623d18080ec7786a..d6d7848a359472f9714997d4458c4dac8d8b60c5 100644 (file)
@@ -52,14 +52,18 @@ class SecurityCapability extends RestCapability<SecurityApi> {
         NAObjectMap<HomeDataModule> modules = homeData.getModules();
         handler.getActiveChildren().forEach(childHandler -> {
             String childId = childHandler.getId();
-            persons.getOpt(childId).ifPresentOrElse(person -> {
-                person.setIgnoredForThingUpdate(true);
-                childHandler.setNewData(person);
+            persons.getOpt(childId).ifPresentOrElse(personData -> {
+                personData.setIgnoredForThingUpdate(true);
+                childHandler.setNewData(personData);
             }, () -> {
-                modules.getOpt(childId).ifPresent(module -> {
-                    module.setIgnoredForThingUpdate(true);
-                    childHandler.setNewData(module);
+                modules.getOpt(childId).ifPresent(childData -> {
+                    childData.setIgnoredForThingUpdate(true);
+                    childHandler.setNewData(childData);
                 });
+                modules.values().stream().filter(module -> childId.equals(module.getBridge()))
+                        .forEach(bridgedModule -> {
+                            childHandler.setNewData(bridgedModule);
+                        });
             });
         });
     }
@@ -70,8 +74,15 @@ class SecurityCapability extends RestCapability<SecurityApi> {
         NAObjectMap<HomeStatusModule> modules = homeStatus.getModules();
         handler.getActiveChildren().forEach(childHandler -> {
             String childId = childHandler.getId();
-            persons.getOpt(childId).ifPresentOrElse(person -> childHandler.setNewData(person), () -> {
-                modules.getOpt(childId).ifPresentOrElse(module -> childHandler.setNewData(module), () -> {
+            persons.getOpt(childId).ifPresentOrElse(personData -> childHandler.setNewData(personData), () -> {
+                modules.getOpt(childId).ifPresentOrElse(childData -> {
+                    childHandler.setNewData(childData);
+                    modules.values().stream().filter(module -> childId.equals(module.getBridge()))
+                            .forEach(bridgedModule -> {
+                                childHandler.setNewData(bridgedModule);
+                            });
+
+                }, () -> {
                     // This module is not present in the homestatus data, so it is considered as unreachable
                     HomeStatusModule module = new HomeStatusModule();
                     module.setReachable(false);
diff --git a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/channelhelper/SirenChannelHelper.java b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/channelhelper/SirenChannelHelper.java
new file mode 100644 (file)
index 0000000..96ccdc7
--- /dev/null
@@ -0,0 +1,52 @@
+/**
+ * Copyright (c) 2010-2022 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.channelhelper;
+
+import static org.openhab.binding.netatmo.internal.NetatmoBindingConstants.*;
+import static org.openhab.binding.netatmo.internal.utils.ChannelTypeUtils.toStringType;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.netatmo.internal.api.dto.HomeStatusModule;
+import org.openhab.binding.netatmo.internal.api.dto.NAThing;
+import org.openhab.core.config.core.Configuration;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+
+/**
+ * The {@link SirenChannelHelper} handles specific behavior of the siren module
+ *
+ * @author Gaël L'hopital - Initial contribution
+ *
+ */
+@NonNullByDefault
+public class SirenChannelHelper extends ChannelHelper {
+
+    public SirenChannelHelper() {
+        super(GROUP_SIREN);
+    }
+
+    @Override
+    protected @Nullable State internalGetProperty(String channelId, NAThing naThing, Configuration config) {
+        if (naThing instanceof HomeStatusModule) {
+            HomeStatusModule homeStatus = (HomeStatusModule) naThing;
+            switch (channelId) {
+                case CHANNEL_MONITORING:
+                    return homeStatus.getMonitoring();
+                case CHANNEL_STATUS:
+                    return homeStatus.getStatus().map(status -> toStringType(status)).orElse(UnDefType.UNDEF);
+            }
+        }
+        return null;
+    }
+}
index 6f323041ddd594bdc575e8c574453ff8f4e16512..042d1972e9931cb811619ee60160e9b9e52abdc8 100644 (file)
@@ -55,7 +55,6 @@ channel-group-type.netatmo.noise.label = Noise
 channel-group-type.netatmo.person.label = Person
 channel-group-type.netatmo.person.channel.last-seen.label = Last Seen
 channel-group-type.netatmo.person.channel.last-seen.description = Moment when this person was last seen.
-channel-group-type.netatmo.plug.label = Thermostat Plug
 channel-group-type.netatmo.presence.label = Presence Camera
 channel-group-type.netatmo.pressure-extended.label = Pressure
 channel-group-type.netatmo.pressure-extended.channel.trend.label = Pressure Trend
@@ -74,6 +73,7 @@ channel-group-type.netatmo.setpoint.channel.end.description = End time of the cu
 channel-group-type.netatmo.setpoint.channel.start.label = Setpoint Start
 channel-group-type.netatmo.setpoint.channel.start.description = Start time of the currently applied setpoint.
 channel-group-type.netatmo.signal.label = Signal
+channel-group-type.netatmo.siren.label = Siren Status
 channel-group-type.netatmo.status-doorbell.label = Camera Status
 channel-group-type.netatmo.status.label = Camera Status
 channel-group-type.netatmo.sub-event-doorbell.label = Sub Event
@@ -268,6 +268,12 @@ channel-type.netatmo.setpoint-duration.label = Setpoint Duration
 channel-type.netatmo.setpoint-duration.description = Default duration of manual setpoint changes.
 channel-type.netatmo.setpoint.label = Setpoint
 channel-type.netatmo.setpoint.description = Thermostat temperature setpoint.
+channel-type.netatmo.siren-monitoring.label = Monitoring
+channel-type.netatmo.siren-monitoring.description = Monitoring state of the equipment
+channel-type.netatmo.siren-status.label = Status
+channel-type.netatmo.siren-status.description = Status of the siren
+channel-type.netatmo.siren-status.state.option.no_sound = Silent
+channel-type.netatmo.siren-status.state.option.sound = Alarm
 channel-type.netatmo.th-mode.label = Thermostat Mode
 channel-type.netatmo.th-mode.description = Chosen thermostat mode (home, frost guard, manual, max).
 channel-type.netatmo.th-mode.state.option.HOME = Home
index 2abba902ff11a7120860f2af0036ead48ec2244e..4b491de83312fff3e0102f5f4c13a5a615ea5bad 100644 (file)
@@ -55,7 +55,6 @@ channel-group-type.netatmo.noise.label = Rumore
 channel-group-type.netatmo.person.label = Persona
 channel-group-type.netatmo.person.channel.last-seen.label = Visto l'ultima volta
 channel-group-type.netatmo.person.channel.last-seen.description = Momento quando questa persona è stata vista per l'ultima volta.
-channel-group-type.netatmo.plug.label = Spina Termostato
 channel-group-type.netatmo.presence.label = Fotocamera Presenza
 channel-group-type.netatmo.pressure-extended.label = Pressione
 channel-group-type.netatmo.pressure-extended.channel.trend.label = Grafico Pressione
index 9c174af72ffaef9b56152590287554c4cd118afc..25071010116ce45941e8328c802ddf9b9a88cdbc 100644 (file)
                <description>Monitoring state of the camera</description>
        </channel-type>
 
+       <channel-type id="siren-monitoring">
+               <item-type>Switch</item-type>
+               <label>Monitoring</label>
+               <description>Monitoring state of the equipment</description>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="siren-status">
+               <item-type>String</item-type>
+               <label>Status</label>
+               <description>Status of the siren</description>
+               <state pattern="%s" readOnly="true">
+                       <options>
+                               <option value="no_sound">Silent</option>
+                               <option value="sound">Alarm</option>
+                       </options>
+               </state>
+       </channel-type>
+
        <channel-type id="window-open">
                <item-type>Contact</item-type>
                <label>Window Status</label>
index 29a76de12866bd6ef09c811249e2230c9a5868a7..98597d4e5c7473e1a4de8bca1b1b6fceef78251d 100644 (file)
@@ -4,13 +4,6 @@
        xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
        xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
 
-       <channel-group-type id="plug">
-               <label>Thermostat Plug</label>
-               <channels>
-                       <channel id="boiler-status" typeId="boiler-status"/>
-               </channels>
-       </channel-group-type>
-
        <channel-group-type id="temperature-room">
                <label>Room Temperature</label>
                <channels>
index 82e2483ee45efedfe0787b25a66ce956f5863232..37214aaba7b0c50a7307a872f7fbf806c846903c 100644 (file)
                </channels>
        </channel-group-type>
 
+       <channel-group-type id="siren">
+               <label>Siren Status</label>
+               <channels>
+                       <channel id="status" typeId="siren-status"/>
+                       <channel id="monitoring" typeId="siren-monitoring"/>
+               </channels>
+       </channel-group-type>
+
        <channel-group-type id="status-doorbell">
                <label>Camera Status</label>
                <channels>