[Netatmo] Binding rewrite without external dependencies - step 3 (#12357)

Signed-off-by: clinique <gael@lhopital.org>
Also-by: Laurent Garnier <lg.hc@free.fr>
This commit is contained in:
Gaël L'hopital 2022-05-07 00:37:24 +02:00 committed by GitHub
parent 3b793a73a1
commit adda4c8769
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
173 changed files with 10918 additions and 9171 deletions

View File

@ -11,40 +11,3 @@ https://www.eclipse.org/legal/epl-2.0/.
== Source Code
https://github.com/openhab/openhab-addons
== Third-party Content
commons-codec
* License: Apache 2.0 License
* Project; https://commons.apache.org/proper/commons-codec
* Source: https://commons.apache.org/proper/commons-codec
gson-fire
* License: Apache 2.0 License
* Project: http://gsonfire.io
* Source: https://github.com/julman99/gson-fire
json
* License: JSON License
* Project: https://www.json.org
* Source: https://github.com/douglascrockford/JSON-java
okhttp
* License: Apache 2.0 License
* Project: https://square.github.io/okhttp
* Source: https://github.com/square/okhttp
okio
* License: Apache 2.0 License
* Project: https://square.github.io/okio/2.x/okio/jvm/okio
* Source: https://github.com/square/okio
oltu.oauth2
* License: Apache 2.0 License
* Project: https://oltu.apache.org
* Source: https://svn.apache.org/viewvc/oltu/trunk
netatmo-swagger-decl
* License: MIT License
* Project: https://dev.netatmo.com
* Source: https://github.com/cbornet/netatmo-swagger-decl

File diff suppressed because it is too large Load Diff

View File

@ -14,114 +14,4 @@
<name>openHAB Add-ons :: Bundles :: Netatmo Binding</name>
<properties>
<bnd.importpackage>!android.*,!com.android.org.*,!org.apache.harmony.*,!sun.*,!org.apache.oltu.*</bnd.importpackage>
</properties>
<dependencies>
<dependency>
<groupId>org.openhab.osgiify</groupId>
<artifactId>org.json.json</artifactId>
<version>20131018</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.squareup.okhttp</groupId>
<artifactId>okhttp</artifactId>
<version>2.7.5</version>
<scope>compile</scope>
<exclusions>
<exclusion>
<groupId>com.google.android</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.squareup.okhttp</groupId>
<artifactId>logging-interceptor</artifactId>
<version>2.7.5</version>
<scope>compile</scope>
<exclusions>
<exclusion>
<groupId>com.google.android</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.squareup.okio</groupId>
<artifactId>okio</artifactId>
<version>1.6.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>io.gsonfire</groupId>
<artifactId>gson-fire</artifactId>
<version>1.8.4</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.apache.oltu.oauth2</groupId>
<artifactId>org.apache.oltu.oauth2.client</artifactId>
<version>1.0.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.apache.oltu.oauth2</groupId>
<artifactId>org.apache.oltu.oauth2.common</artifactId>
<version>1.0.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.8</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.5</version>
<scope>compile</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>io.swagger.codegen.v3</groupId>
<artifactId>swagger-codegen-maven-plugin</artifactId>
<version>3.0.21</version>
<executions>
<execution>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<inputSpec>https://raw.githubusercontent.com/cbornet/netatmo-swagger-decl/35e27745fb0d432bc6c8b5ec7a83ed2a09944cea/spec/swagger.yaml</inputSpec>
<language>java</language>
<generateApiTests>false</generateApiTests>
<generateModelTests>false</generateModelTests>
<configOptions>
<sourceFolder>src/main/java</sourceFolder>
<java8>true</java8>
<dateLibrary>java8-localdatetime</dateLibrary>
<useRuntimeException>true</useRuntimeException>
</configOptions>
</configuration>
</execution>
</executions>
<dependencies>
<dependency>
<!-- Required for JDK 17 compatibility, see: https://github.com/swagger-api/swagger-codegen/issues/11253 -->
<groupId>com.github.jknack</groupId>
<artifactId>handlebars</artifactId>
<version>4.3.0</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
</project>

View File

@ -4,7 +4,6 @@
<feature name="openhab-binding-netatmo" description="Netatmo Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<bundle dependency="true">mvn:org.openhab.osgiify/org.json.json/20131018</bundle>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.netatmo/${project.version}</bundle>
</feature>
</features>

View File

@ -1,41 +0,0 @@
/**
* 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;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* {@link APIUtils} provides util methods for the usage of the generated API classes.
*
* @author Sven Strohschein - Initial contribution
*/
@NonNullByDefault
public final class APIUtils {
private APIUtils() {
}
public static <T> Stream<T> nonNullStream(Collection<T> collection) {
return Optional.ofNullable(collection).stream().flatMap(Collection::stream);
}
public static <T> List<T> nonNullList(List<T> list) {
return Optional.ofNullable(list).orElse(Collections.emptyList());
}
}

View File

@ -1,138 +0,0 @@
/**
* 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;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import javax.measure.Unit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.io.net.http.HttpUtil;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.RawType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
/**
* This class holds various channel values conversion methods
*
* @author Gaël L'hopital - Initial contribution
* @author Rob Nielsen - Added day, week, and month measurements to the weather station and modules
*
*/
@NonNullByDefault
public class ChannelTypeUtils {
public static State toStringType(@Nullable String value) {
return (value == null) ? UnDefType.NULL : new StringType(value);
}
public static ZonedDateTime toZonedDateTime(Integer netatmoTS, ZoneId zoneId) {
Instant i = Instant.ofEpochSecond(netatmoTS);
return ZonedDateTime.ofInstant(i, zoneId);
}
public static State toDateTimeType(@Nullable Float netatmoTS, ZoneId zoneId) {
return netatmoTS == null ? UnDefType.NULL : toDateTimeType(toZonedDateTime(netatmoTS.intValue(), zoneId));
}
public static State toDateTimeType(@Nullable Integer netatmoTS, ZoneId zoneId) {
return netatmoTS == null ? UnDefType.NULL : toDateTimeType(toZonedDateTime(netatmoTS, zoneId));
}
public static State toDateTimeType(@Nullable ZonedDateTime zonedDateTime) {
return (zonedDateTime == null) ? UnDefType.NULL : new DateTimeType(zonedDateTime);
}
public static State toDecimalType(@Nullable Float value) {
return (value == null) ? UnDefType.NULL : toDecimalType(new BigDecimal(value));
}
public static State toDecimalType(@Nullable Integer value) {
return (value == null) ? UnDefType.NULL : toDecimalType(new BigDecimal(value));
}
public static State toDecimalType(@Nullable Double value) {
return (value == null) ? UnDefType.NULL : toDecimalType(new BigDecimal(value));
}
public static State toDecimalType(float value) {
return toDecimalType(new BigDecimal(value));
}
public static State toDecimalType(double value) {
return toDecimalType(new BigDecimal(value));
}
public static State toDecimalType(@Nullable BigDecimal decimal) {
return decimal == null ? UnDefType.NULL : new DecimalType(decimal.setScale(2, RoundingMode.HALF_UP));
}
public static State toDecimalType(@Nullable String textualDecimal) {
return textualDecimal == null ? UnDefType.NULL : new DecimalType(textualDecimal);
}
public static State toOnOffType(@Nullable String yesno) {
return "on".equalsIgnoreCase(yesno) ? OnOffType.ON : OnOffType.OFF;
}
public static State toOnOffType(@Nullable Integer value) {
return value != null ? (value == 1 ? OnOffType.ON : OnOffType.OFF) : UnDefType.UNDEF;
}
public static State toOnOffType(@Nullable Boolean value) {
return value != null ? (value ? OnOffType.ON : OnOffType.OFF) : UnDefType.UNDEF;
}
public static State toQuantityType(@Nullable Float value, Unit<?> unit) {
return value == null ? UnDefType.NULL : toQuantityType(new BigDecimal(value), unit);
}
public static State toQuantityType(@Nullable Integer value, Unit<?> unit) {
return value == null ? UnDefType.NULL : toQuantityType(new BigDecimal(value), unit);
}
public static State toQuantityType(@Nullable Double value, Unit<?> unit) {
return value == null ? UnDefType.NULL : toQuantityType(new BigDecimal(value), unit);
}
public static State toQuantityType(float value, Unit<?> unit) {
return toQuantityType(new BigDecimal(value), unit);
}
public static State toQuantityType(int value, Unit<?> unit) {
return toQuantityType(new BigDecimal(value), unit);
}
public static State toQuantityType(double value, Unit<?> unit) {
return toQuantityType(new BigDecimal(value), unit);
}
public static State toQuantityType(@Nullable BigDecimal value, Unit<?> unit) {
return value == null ? UnDefType.NULL : new QuantityType<>(value, unit);
}
public static State toRawType(String pictureUrl) {
RawType picture = HttpUtil.downloadImage(pictureUrl);
return picture == null ? UnDefType.UNDEF : picture;
}
}

View File

@ -12,292 +12,127 @@
*/
package org.openhab.binding.netatmo.internal;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.netatmo.internal.webhook.NAWebhookCameraEvent.EventTypeEnum;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link NetatmoBinding} class defines common constants, which are used
* The {@link NetatmoBindingConstants} class defines common constants, which are used
* across the whole binding.
*
* @author Gaël L'hopital - Initial contribution
* @author Rob Nielsen - Added day, week, and month measurements to the weather station and modules
*
*/
@NonNullByDefault
public class NetatmoBindingConstants {
private static final String BINDING_ID = "netatmo";
public static final String BINDING_ID = "netatmo";
public static final String VENDOR = "Netatmo";
// Configuration keys
public static final String EQUIPMENT_ID = "id";
public static final String PARENT_ID = "parentId";
public static final String REFRESH_INTERVAL = "refreshInterval";
public static final String SETPOINT_DEFAULT_DURATION = "setpointDefaultDuration";
public static final String WEBHOOK_APP = "app_security";
// Things properties
public static final String PROPERTY_CITY = "city";
public static final String PROPERTY_COUNTRY = "country";
public static final String PROPERTY_TIMEZONE = "timezone";
public static final String PROPERTY_FEATURE = "feature";
// Scale for Weather Station /getmeasure
public static final String THIRTY_MINUTES = "30min";
public static final String ONE_HOUR = "1hour";
public static final String THREE_HOURS = "3hours";
public static final String ONE_DAY = "1day";
public static final String ONE_WEEK = "1week";
public static final String ONE_MONTH = "1month";
// Channel group ids
public static final String GROUP_LAST_EVENT = "last-event";
public static final String GROUP_TEMPERATURE = "temperature";
public static final String GROUP_HUMIDITY = "humidity";
public static final String GROUP_AIR_QUALITY = "airquality";
public static final String GROUP_NOISE = "noise";
public static final String GROUP_PRESSURE = "pressure";
public static final String GROUP_TIMESTAMP = "timestamp";
public static final String GROUP_RAIN = "rain";
public static final String GROUP_WIND = "wind";
public static final String GROUP_ENERGY = "energy";
public static final String GROUP_SIGNAL = "signal";
public static final String GROUP_BATTERY = "battery";
public static final String GROUP_SECURITY = "security";
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_PERSON = "person";
public static final String GROUP_PERSON_EVENT = "person-event";
public static final String GROUP_ROOM_TEMPERATURE = "room-temperature";
public static final String GROUP_ROOM_PROPERTIES = "room-properties";
public static final String GROUP_TH_PROPERTIES = "th-properties";
public static final String GROUP_TH_SETPOINT = "setpoint";
public static final String GROUP_LOCATION = "location";
// Type for Weather Station /getmeasure
public static final String DATE_MIN_CO2 = "date_min_co2";
public static final String DATE_MAX_CO2 = "date_max_co2";
public static final String DATE_MIN_HUM = "date_min_hum";
public static final String DATE_MAX_HUM = "date_max_hum";
public static final String DATE_MIN_NOISE = "date_min_noise";
public static final String DATE_MAX_NOISE = "date_max_noise";
public static final String DATE_MIN_PRESSURE = "date_min_pressure";
public static final String DATE_MAX_PRESSURE = "date_max_pressure";
public static final String DATE_MIN_TEMP = "date_min_temp";
public static final String DATE_MAX_TEMP = "date_max_temp";
public static final String MIN_CO2 = "min_co2";
public static final String MAX_CO2 = "max_co2";
public static final String MIN_HUM = "min_hum";
public static final String MAX_HUM = "max_hum";
public static final String MIN_NOISE = "min_noise";
public static final String MAX_NOISE = "max_noise";
public static final String MIN_PRESSURE = "min_pressure";
public static final String MAX_PRESSURE = "max_pressure";
public static final String MIN_TEMP = "min_temp";
public static final String MAX_TEMP = "max_temp";
public static final String SUM_RAIN = "sum_rain";
// Alternative extended groups
public static final String OPTION_EXTENDED = "-extended";
public static final String OPTION_OUTSIDE = "-outside";
public static final String GROUP_TYPE_TIMESTAMP_EXTENDED = GROUP_TIMESTAMP + OPTION_EXTENDED;
public static final String GROUP_TYPE_BATTERY_EXTENDED = GROUP_BATTERY + OPTION_EXTENDED;
public static final String GROUP_TYPE_PRESSURE_EXTENDED = GROUP_PRESSURE + OPTION_EXTENDED;
public static final String GROUP_TYPE_TEMPERATURE_EXTENDED = GROUP_TEMPERATURE + OPTION_EXTENDED;
public static final String GROUP_TYPE_AIR_QUALITY_EXTENDED = GROUP_AIR_QUALITY + OPTION_EXTENDED;
public static final String GROUP_TYPE_TEMPERATURE_OUTSIDE = GROUP_TEMPERATURE + OPTION_OUTSIDE;
// List of Bridge Type UIDs
public static final ThingTypeUID APIBRIDGE_THING_TYPE = new ThingTypeUID(BINDING_ID, "netatmoapi");
// List of Weather Station Things Type UIDs
public static final ThingTypeUID MAIN_THING_TYPE = new ThingTypeUID(BINDING_ID, "NAMain");
public static final ThingTypeUID MODULE1_THING_TYPE = new ThingTypeUID(BINDING_ID, "NAModule1");
public static final ThingTypeUID MODULE2_THING_TYPE = new ThingTypeUID(BINDING_ID, "NAModule2");
public static final ThingTypeUID MODULE3_THING_TYPE = new ThingTypeUID(BINDING_ID, "NAModule3");
public static final ThingTypeUID MODULE4_THING_TYPE = new ThingTypeUID(BINDING_ID, "NAModule4");
// Netatmo Health Coach
public static final ThingTypeUID HOMECOACH_THING_TYPE = new ThingTypeUID(BINDING_ID, "NHC");
// List of Thermostat Things Type UIDs
public static final ThingTypeUID PLUG_THING_TYPE = new ThingTypeUID(BINDING_ID, "NAPlug");
public static final ThingTypeUID THERM1_THING_TYPE = new ThingTypeUID(BINDING_ID, "NATherm1");
// List of Welcome Home Things Type UIDs
public static final ThingTypeUID WELCOME_HOME_THING_TYPE = new ThingTypeUID(BINDING_ID, "NAWelcomeHome");
public static final ThingTypeUID WELCOME_CAMERA_THING_TYPE = new ThingTypeUID(BINDING_ID, "NACamera");
public static final ThingTypeUID WELCOME_PERSON_THING_TYPE = new ThingTypeUID(BINDING_ID, "NAWelcomePerson");
// Presence camera
public static final ThingTypeUID PRESENCE_CAMERA_THING_TYPE = new ThingTypeUID(BINDING_ID, "NOC");
// Weather Station Channel ids
public static final String CHANNEL_TEMPERATURE = "Temperature";
public static final String CHANNEL_TEMP_TREND = "TempTrend";
public static final String CHANNEL_HUMIDITY = "Humidity";
public static final String CHANNEL_MAX_HUMIDITY = "MaxHumidity";
public static final String CHANNEL_MAX_HUMIDITY_THIS_WEEK = "MaxHumidityThisWeek";
public static final String CHANNEL_MAX_HUMIDITY_THIS_MONTH = "MaxHumidityThisMonth";
public static final String CHANNEL_MIN_HUMIDITY = "MinHumidity";
public static final String CHANNEL_MIN_HUMIDITY_THIS_WEEK = "MinHumidityThisWeek";
public static final String CHANNEL_MIN_HUMIDITY_THIS_MONTH = "MinHumidityThisMonth";
public static final String CHANNEL_HUMIDEX = "Humidex";
public static final String CHANNEL_TIMEUTC = "TimeStamp";
public static final String CHANNEL_DEWPOINT = "Dewpoint";
public static final String CHANNEL_DEWPOINTDEP = "DewpointDepression";
public static final String CHANNEL_HEATINDEX = "HeatIndex";
public static final String CHANNEL_LAST_STATUS_STORE = "LastStatusStore";
public static final String CHANNEL_LAST_MESSAGE = "LastMessage";
public static final String CHANNEL_LOCATION = "Location";
public static final String CHANNEL_DATE_MAX_CO2 = "DateMaxCo2";
public static final String CHANNEL_DATE_MAX_CO2_THIS_WEEK = "DateMaxCo2ThisWeek";
public static final String CHANNEL_DATE_MAX_CO2_THIS_MONTH = "DateMaxCo2ThisMonth";
public static final String CHANNEL_DATE_MIN_CO2 = "DateMinCo2";
public static final String CHANNEL_DATE_MIN_CO2_THIS_WEEK = "DateMinCo2ThisWeek";
public static final String CHANNEL_DATE_MIN_CO2_THIS_MONTH = "DateMinCo2ThisMonth";
public static final String CHANNEL_DATE_MAX_HUMIDITY = "DateMaxHumidity";
public static final String CHANNEL_DATE_MAX_HUMIDITY_THIS_WEEK = "DateMaxHumidityThisWeek";
public static final String CHANNEL_DATE_MAX_HUMIDITY_THIS_MONTH = "DateMaxHumidityThisMonth";
public static final String CHANNEL_DATE_MIN_HUMIDITY = "DateMinHumidity";
public static final String CHANNEL_DATE_MIN_HUMIDITY_THIS_WEEK = "DateMinHumidityThisWeek";
public static final String CHANNEL_DATE_MIN_HUMIDITY_THIS_MONTH = "DateMinHumidityThisMonth";
public static final String CHANNEL_DATE_MAX_NOISE = "DateMaxNoise";
public static final String CHANNEL_DATE_MAX_NOISE_THIS_WEEK = "DateMaxNoiseThisWeek";
public static final String CHANNEL_DATE_MAX_NOISE_THIS_MONTH = "DateMaxNoiseThisMonth";
public static final String CHANNEL_DATE_MIN_NOISE = "DateMinNoise";
public static final String CHANNEL_DATE_MIN_NOISE_THIS_WEEK = "DateMinNoiseThisWeek";
public static final String CHANNEL_DATE_MIN_NOISE_THIS_MONTH = "DateMinNoiseThisMonth";
public static final String CHANNEL_DATE_MAX_PRESSURE = "DateMaxPressure";
public static final String CHANNEL_DATE_MAX_PRESSURE_THIS_WEEK = "DateMaxPressureThisWeek";
public static final String CHANNEL_DATE_MAX_PRESSURE_THIS_MONTH = "DateMaxPressureThisMonth";
public static final String CHANNEL_DATE_MIN_PRESSURE = "DateMinPressure";
public static final String CHANNEL_DATE_MIN_PRESSURE_THIS_WEEK = "DateMinPressureThisWeek";
public static final String CHANNEL_DATE_MIN_PRESSURE_THIS_MONTH = "DateMinPressureThisMonth";
public static final String CHANNEL_DATE_MAX_TEMP = "DateMaxTemp";
public static final String CHANNEL_DATE_MAX_TEMP_THIS_WEEK = "DateMaxTempThisWeek";
public static final String CHANNEL_DATE_MAX_TEMP_THIS_MONTH = "DateMaxTempThisMonth";
public static final String CHANNEL_DATE_MIN_TEMP = "DateMinTemp";
public static final String CHANNEL_DATE_MIN_TEMP_THIS_WEEK = "DateMinTempThisWeek";
public static final String CHANNEL_DATE_MIN_TEMP_THIS_MONTH = "DateMinTempThisMonth";
public static final String CHANNEL_MAX_TEMP = "MaxTemp";
public static final String CHANNEL_MAX_TEMP_THIS_WEEK = "MaxTempThisWeek";
public static final String CHANNEL_MAX_TEMP_THIS_MONTH = "MaxTempThisMonth";
public static final String CHANNEL_MIN_TEMP = "MinTemp";
public static final String CHANNEL_MIN_TEMP_THIS_WEEK = "MinTempThisWeek";
public static final String CHANNEL_MIN_TEMP_THIS_MONTH = "MinTempThisMonth";
public static final String CHANNEL_ABSOLUTE_PRESSURE = "AbsolutePressure";
public static final String CHANNEL_CO2 = "Co2";
public static final String CHANNEL_MAX_CO2 = "MaxCo2";
public static final String CHANNEL_MAX_CO2_THIS_WEEK = "MaxCo2ThisWeek";
public static final String CHANNEL_MAX_CO2_THIS_MONTH = "MaxCo2ThisMonth";
public static final String CHANNEL_MIN_CO2 = "MinCo2";
public static final String CHANNEL_MIN_CO2_THIS_WEEK = "MinCo2ThisWeek";
public static final String CHANNEL_MIN_CO2_THIS_MONTH = "MinCo2ThisMonth";
public static final String CHANNEL_NOISE = "Noise";
public static final String CHANNEL_MAX_NOISE = "MaxNoise";
public static final String CHANNEL_MAX_NOISE_THIS_WEEK = "MaxNoiseThisWeek";
public static final String CHANNEL_MAX_NOISE_THIS_MONTH = "MaxNoiseThisMonth";
public static final String CHANNEL_MIN_NOISE = "MinNoise";
public static final String CHANNEL_MIN_NOISE_THIS_WEEK = "MinNoiseThisWeek";
public static final String CHANNEL_MIN_NOISE_THIS_MONTH = "MinNoiseThisMonth";
public static final String CHANNEL_PRESSURE = "Pressure";
public static final String CHANNEL_MAX_PRESSURE = "MaxPressure";
public static final String CHANNEL_MAX_PRESSURE_THIS_WEEK = "MaxPressureThisWeek";
public static final String CHANNEL_MAX_PRESSURE_THIS_MONTH = "MaxPressureThisMonth";
public static final String CHANNEL_MIN_PRESSURE = "MinPressure";
public static final String CHANNEL_MIN_PRESSURE_THIS_WEEK = "MinPressureThisWeek";
public static final String CHANNEL_MIN_PRESSURE_THIS_MONTH = "MinPressureThisMonth";
public static final String CHANNEL_PRESS_TREND = "PressTrend";
public static final String CHANNEL_RAIN = "Rain";
public static final String CHANNEL_SUM_RAIN1 = "SumRain1";
public static final String CHANNEL_SUM_RAIN24 = "SumRain24";
public static final String CHANNEL_SUM_RAIN_THIS_WEEK = "SumRainThisWeek";
public static final String CHANNEL_SUM_RAIN_THIS_MONTH = "SumRainThisMonth";
public static final String CHANNEL_WIND_ANGLE = "WindAngle";
public static final String CHANNEL_WIND_STRENGTH = "WindStrength";
public static final String CHANNEL_MAX_WIND_STRENGTH = "MaxWindStrength";
public static final String CHANNEL_DATE_MAX_WIND_STRENGTH = "DateMaxWindStrength";
public static final String CHANNEL_GUST_ANGLE = "GustAngle";
public static final String CHANNEL_GUST_STRENGTH = "GustStrength";
public static final String CHANNEL_LOW_BATTERY = "LowBattery";
public static final String CHANNEL_BATTERY_LEVEL = "BatteryVP";
public static final String CHANNEL_WIFI_STATUS = "WifiStatus";
public static final String CHANNEL_RF_STATUS = "RfStatus";
// Healthy Home Coach specific channel
public static final String CHANNEL_HEALTH_INDEX = "HealthIndex";
// Thermostat specific channels
public static final String CHANNEL_SETPOINT_MODE = "SetpointMode";
public static final String CHANNEL_SETPOINT_END_TIME = "SetpointEndTime";
public static final String CHANNEL_SETPOINT_TEMP = "Sp_Temperature";
public static final String CHANNEL_THERM_RELAY = "ThermRelayCmd";
public static final String CHANNEL_THERM_ORIENTATION = "ThermOrientation";
public static final String CHANNEL_CONNECTED_BOILER = "ConnectedBoiler";
public static final String CHANNEL_LAST_PLUG_SEEN = "LastPlugSeen";
public static final String CHANNEL_LAST_BILAN = "LastBilan";
public static final String CHANNEL_PLANNING = "Planning";
public static final String CHANNEL_SETPOINT_MODE_MANUAL = "manual";
public static final String CHANNEL_SETPOINT_MODE_AWAY = "away";
public static final String CHANNEL_SETPOINT_MODE_HG = "hg";
public static final String CHANNEL_SETPOINT_MODE_OFF = "off";
public static final String CHANNEL_SETPOINT_MODE_MAX = "max";
public static final String CHANNEL_SETPOINT_MODE_PROGRAM = "program";
// Module Properties
public static final String PROPERTY_SIGNAL_LEVELS = "signalLevels";
public static final String PROPERTY_BATTERY_LEVELS = "batteryLevels";
public static final String PROPERTY_REFRESH_PERIOD = "refreshPeriod";
// Welcome Home specific channels
public static final String CHANNEL_WELCOME_HOME_CITY = "welcomeHomeCity";
public static final String CHANNEL_WELCOME_HOME_COUNTRY = "welcomeHomeCountry";
public static final String CHANNEL_WELCOME_HOME_TIMEZONE = "welcomeHomeTimezone";
public static final String CHANNEL_WELCOME_HOME_PERSONCOUNT = "welcomeHomePersonCount";
public static final String CHANNEL_WELCOME_HOME_UNKNOWNCOUNT = "welcomeHomeUnknownCount";
public static final String CHANNEL_WELCOME_HOME_EVENT = "welcomeHomeEvent";
public static final String CHANNEL_CAMERA_EVENT = "cameraEvent";
public static final String CHANNEL_WELCOME_PERSON_LASTSEEN = "welcomePersonLastSeen";
public static final String CHANNEL_WELCOME_PERSON_ATHOME = "welcomePersonAtHome";
public static final String CHANNEL_WELCOME_PERSON_AVATAR_URL = "welcomePersonAvatarUrl";
public static final String CHANNEL_WELCOME_PERSON_AVATAR = "welcomePersonAvatar";
public static final String CHANNEL_WELCOME_PERSON_LASTMESSAGE = "welcomePersonLastEventMessage";
public static final String CHANNEL_WELCOME_PERSON_LASTTIME = "welcomePersonLastEventTime";
public static final String CHANNEL_WELCOME_PERSON_LASTEVENT = "welcomePersonLastEvent";
public static final String CHANNEL_WELCOME_PERSON_LASTEVENT_URL = "welcomePersonLastEventUrl";
public static final String CHANNEL_WELCOME_CAMERA_STATUS = "welcomeCameraStatus";
public static final String CHANNEL_WELCOME_CAMERA_SDSTATUS = "welcomeCameraSdStatus";
public static final String CHANNEL_WELCOME_CAMERA_ALIMSTATUS = "welcomeCameraAlimStatus";
public static final String CHANNEL_WELCOME_CAMERA_ISLOCAL = "welcomeCameraIsLocal";
public static final String CHANNEL_WELCOME_CAMERA_LIVEPICTURE = "welcomeCameraLivePicture";
public static final String CHANNEL_WELCOME_CAMERA_LIVEPICTURE_URL = "welcomeCameraLivePictureUrl";
public static final String CHANNEL_WELCOME_CAMERA_LIVESTREAM_URL = "welcomeCameraLiveStreamUrl";
public static final String CHANNEL_WELCOME_EVENT_TYPE = "welcomeEventType";
public static final String CHANNEL_WELCOME_EVENT_TIME = "welcomeEventTime";
public static final String CHANNEL_WELCOME_EVENT_CAMERAID = "welcomeEventCameraId";
public static final String CHANNEL_WELCOME_EVENT_PERSONID = "welcomeEventPersonId";
public static final String CHANNEL_WELCOME_EVENT_SNAPSHOT = "welcomeEventSnapshot";
public static final String CHANNEL_WELCOME_EVENT_SNAPSHOT_URL = "welcomeEventSnapshotURL";
public static final String CHANNEL_WELCOME_EVENT_VIDEO_URL = "welcomeEventVideoURL";
public static final String CHANNEL_WELCOME_EVENT_VIDEOSTATUS = "welcomeEventVideoStatus";
public static final String CHANNEL_WELCOME_EVENT_ISARRIVAL = "welcomeEventIsArrival";
public static final String CHANNEL_WELCOME_EVENT_MESSAGE = "welcomeEventMessage";
public static final String CHANNEL_WELCOME_EVENT_SUBTYPE = "welcomeEventSubType";
// Camera specific channels
public static final String CHANNEL_CAMERA_STATUS = "cameraStatus";
public static final String CHANNEL_CAMERA_SDSTATUS = "cameraSdStatus";
public static final String CHANNEL_CAMERA_ALIMSTATUS = "cameraAlimStatus";
public static final String CHANNEL_CAMERA_ISLOCAL = "cameraIsLocal";
public static final String CHANNEL_CAMERA_LIVEPICTURE = "cameraLivePicture";
public static final String CHANNEL_CAMERA_LIVEPICTURE_URL = "cameraLivePictureUrl";
public static final String CHANNEL_CAMERA_LIVESTREAM_URL = "cameraLiveStreamUrl";
public static final String WELCOME_PICTURE_URL = "https://api.netatmo.com/api/getcamerapicture";
public static final String WELCOME_PICTURE_IMAGEID = "image_id";
public static final String WELCOME_PICTURE_KEY = "key";
// Presence outdoor camera specific channels
public static final String CHANNEL_CAMERA_FLOODLIGHT_AUTO_MODE = "cameraFloodlightAutoMode";
public static final String CHANNEL_CAMERA_FLOODLIGHT = "cameraFloodlight";
// List of all supported physical devices and modules
public static final Set<ThingTypeUID> SUPPORTED_DEVICE_THING_TYPES_UIDS = Stream
.of(MAIN_THING_TYPE, MODULE1_THING_TYPE, MODULE2_THING_TYPE, MODULE3_THING_TYPE, MODULE4_THING_TYPE,
HOMECOACH_THING_TYPE, PLUG_THING_TYPE, THERM1_THING_TYPE, WELCOME_HOME_THING_TYPE,
WELCOME_CAMERA_THING_TYPE, WELCOME_PERSON_THING_TYPE, PRESENCE_CAMERA_THING_TYPE)
.collect(Collectors.toSet());
// List of all adressable things in OH = SUPPORTED_DEVICE_THING_TYPES_UIDS + the virtual bridge
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Stream
.concat(SUPPORTED_DEVICE_THING_TYPES_UIDS.stream(), Stream.of(APIBRIDGE_THING_TYPE))
.collect(Collectors.toSet());
public static final Set<EventTypeEnum> HOME_EVENTS = Stream.of(EventTypeEnum.PERSON_AWAY)
.collect(Collectors.toSet());
public static final Set<EventTypeEnum> WELCOME_EVENTS = Stream
.of(EventTypeEnum.PERSON, EventTypeEnum.MOVEMENT, EventTypeEnum.CONNECTION, EventTypeEnum.DISCONNECTION,
EventTypeEnum.ON, EventTypeEnum.OFF, EventTypeEnum.BOOT, EventTypeEnum.SD, EventTypeEnum.ALIM,
EventTypeEnum.NEW_MODULE, EventTypeEnum.MODULE_CONNECT, EventTypeEnum.MODULE_DISCONNECT,
EventTypeEnum.MODULE_LOW_BATTERY, EventTypeEnum.MODULE_END_UPDATE, EventTypeEnum.TAG_BIG_MOVE,
EventTypeEnum.TAG_SMALL_MOVE, EventTypeEnum.TAG_UNINSTALLED, EventTypeEnum.TAG_OPEN)
.collect(Collectors.toSet());
public static final Set<EventTypeEnum> PERSON_EVENTS = Stream.of(EventTypeEnum.PERSON, EventTypeEnum.PERSON_AWAY)
.collect(Collectors.toSet());
public static final Set<EventTypeEnum> PRESENCE_EVENTS = Stream
.of(EventTypeEnum.OUTDOOR, EventTypeEnum.ALIM, EventTypeEnum.DAILY_SUMMARY).collect(Collectors.toSet());
// Channel ids
public static final String CHANNEL_VALUE = "value";
public static final String CHANNEL_TREND = "trend";
public static final String CHANNEL_MAX_TIME = "max-time";
public static final String CHANNEL_MIN_TIME = "min-time";
public static final String CHANNEL_MAX_VALUE = "max-today";
public static final String CHANNEL_MIN_VALUE = "min-today";
public static final String CHANNEL_HUMIDEX = "humidex";
public static final String CHANNEL_CO2 = "co2";
public static final String CHANNEL_HEALTH_INDEX = "health-index";
public static final String CHANNEL_HUMIDEX_SCALE = "humidex-scale";
public static final String CHANNEL_DEWPOINT = "dewpoint";
public static final String CHANNEL_DEWPOINT_DEP = "dewpoint-depression";
public static final String CHANNEL_HEAT_INDEX = "heat-index";
public static final String CHANNEL_ABSOLUTE_PRESSURE = "absolute";
public static final String CHANNEL_LAST_SEEN = "last-seen";
public static final String CHANNEL_MEASURES_TIMESTAMP = "measures";
public static final String CHANNEL_LOW_BATTERY = "low-battery";
public static final String CHANNEL_BATTERY_STATUS = "status";
public static final String CHANNEL_SIGNAL_STRENGTH = "strength";
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_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";
public static final String CHANNEL_GUST_ANGLE = "gust-angle";
public static final String CHANNEL_GUST_STRENGTH = "gust-strength";
public static final String CHANNEL_SETPOINT_MODE = "mode";
public static final String CHANNEL_SETPOINT_START_TIME = "start";
public static final String CHANNEL_SETPOINT_END_TIME = "end";
public static final String CHANNEL_THERM_RELAY = "relay-status";
public static final String CHANNEL_ANTICIPATING = "anticipating";
public static final String CHANNEL_ROOM_WINDOW_OPEN = "window-open";
public static final String CHANNEL_ROOM_HEATING_POWER = "heating-power-request";
public static final String CHANNEL_PLANNING = "planning";
public static final String CHANNEL_PERSON_COUNT = "person-count";
public static final String CHANNEL_UNKNOWN_PERSON_COUNT = "unknown-person-count";
public static final String CHANNEL_UNKNOWN_PERSON_PICTURE = "unknown-person-picture";
public static final String CHANNEL_MONITORING = "monitoring";
public static final String CHANNEL_SD_CARD = "sd-card";
public static final String CHANNEL_ALIM_STATUS = "alim";
public static final String CHANNEL_LIVEPICTURE = "picture";
public static final String CHANNEL_LIVEPICTURE_VPN_URL = "vpn-picture-url";
public static final String CHANNEL_LIVEPICTURE_LOCAL_URL = "local-picture-url";
public static final String CHANNEL_LIVESTREAM_VPN_URL = "vpn-stream-url";
public static final String CHANNEL_LIVESTREAM_LOCAL_URL = "local-stream-url";
public static final String CHANNEL_EVENT_TYPE = "type";
public static final String CHANNEL_EVENT_SUBTYPE = "subtype";
public static final String CHANNEL_EVENT_VIDEO_STATUS = "video-status";
public static final String CHANNEL_EVENT_MESSAGE = "message";
public static final String CHANNEL_EVENT_TIME = "time";
public static final String CHANNEL_EVENT_SNAPSHOT = "snapshot";
public static final String CHANNEL_EVENT_SNAPSHOT_URL = "snapshot-url";
public static final String CHANNEL_EVENT_VIDEO_VPN_URL = "vpn-video-url";
public static final String CHANNEL_EVENT_VIDEO_LOCAL_URL = "local-video-url";
public static final String CHANNEL_EVENT_PERSON_ID = "person-id";
public static final String CHANNEL_EVENT_CAMERA_ID = "camera-id";
public static final String CHANNEL_PERSON_AT_HOME = "at-home";
public static final String CHANNEL_PERSON_AVATAR = "avatar";
public static final String CHANNEL_PERSON_AVATAR_URL = "avatar-url";
public static final String CHANNEL_HOME_EVENT = "home-event";
public static final String CHANNEL_SETPOINT_DURATION = "setpoint-duration";
public static final String CHANNEL_FLOODLIGHT = "floodlight";
}

View File

@ -12,47 +12,46 @@
*/
package org.openhab.binding.netatmo.internal;
import static org.openhab.binding.netatmo.internal.NetatmoBindingConstants.*;
import java.util.Dictionary;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServlet;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.netatmo.internal.discovery.NetatmoModuleDiscoveryService;
import org.openhab.binding.netatmo.internal.handler.NetatmoBridgeHandler;
import org.openhab.binding.netatmo.internal.homecoach.NAHealthyHomeCoachHandler;
import org.openhab.binding.netatmo.internal.presence.NAPresenceCameraHandler;
import org.openhab.binding.netatmo.internal.station.NAMainHandler;
import org.openhab.binding.netatmo.internal.station.NAModule1Handler;
import org.openhab.binding.netatmo.internal.station.NAModule2Handler;
import org.openhab.binding.netatmo.internal.station.NAModule3Handler;
import org.openhab.binding.netatmo.internal.station.NAModule4Handler;
import org.openhab.binding.netatmo.internal.thermostat.NAPlugHandler;
import org.openhab.binding.netatmo.internal.thermostat.NATherm1Handler;
import org.openhab.binding.netatmo.internal.webhook.WelcomeWebHookServlet;
import org.openhab.binding.netatmo.internal.welcome.NAWelcomeCameraHandler;
import org.openhab.binding.netatmo.internal.welcome.NAWelcomeHomeHandler;
import org.openhab.binding.netatmo.internal.welcome.NAWelcomePersonHandler;
import org.openhab.core.config.discovery.DiscoveryService;
import org.openhab.core.i18n.LocaleProvider;
import org.openhab.core.i18n.TimeZoneProvider;
import org.openhab.core.i18n.TranslationProvider;
import org.eclipse.jetty.client.HttpClient;
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;
import org.openhab.binding.netatmo.internal.handler.ApiBridgeHandler;
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.CameraCapability;
import org.openhab.binding.netatmo.internal.handler.capability.Capability;
import org.openhab.binding.netatmo.internal.handler.capability.ChannelHelperCapability;
import org.openhab.binding.netatmo.internal.handler.capability.DeviceCapability;
import org.openhab.binding.netatmo.internal.handler.capability.EventCapability;
import org.openhab.binding.netatmo.internal.handler.capability.HomeCapability;
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.WeatherCapability;
import org.openhab.binding.netatmo.internal.handler.channelhelper.ChannelHelper;
import org.openhab.binding.netatmo.internal.providers.NetatmoDescriptionProvider;
import org.openhab.core.config.core.ConfigParser;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.framework.ServiceRegistration;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Modified;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.http.HttpService;
import org.slf4j.Logger;
@ -68,131 +67,91 @@ import org.slf4j.LoggerFactory;
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.netatmo")
public class NetatmoHandlerFactory extends BaseThingHandlerFactory {
private final Logger logger = LoggerFactory.getLogger(NetatmoHandlerFactory.class);
private final Map<ThingUID, ServiceRegistration<?>> discoveryServiceRegs = new HashMap<>();
private final Map<ThingUID, ServiceRegistration<?>> webHookServiceRegs = new HashMap<>();
private final NetatmoDescriptionProvider stateDescriptionProvider;
private final HttpClient httpClient;
private final NADeserializer deserializer;
private final HttpService httpService;
private final NATherm1StateDescriptionProvider stateDescriptionProvider;
private final TimeZoneProvider timeZoneProvider;
private final LocaleProvider localeProvider;
private final TranslationProvider translationProvider;
private boolean backgroundDiscovery;
private final BindingConfiguration configuration = new BindingConfiguration();
@Activate
public NetatmoHandlerFactory(final @Reference HttpService httpService,
final @Reference NATherm1StateDescriptionProvider stateDescriptionProvider,
final @Reference TimeZoneProvider timeZoneProvider, final @Reference LocaleProvider localeProvider,
final @Reference TranslationProvider translationProvider) {
this.httpService = httpService;
public NetatmoHandlerFactory(@Reference NetatmoDescriptionProvider stateDescriptionProvider,
@Reference HttpClientFactory factory, @Reference NADeserializer deserializer,
@Reference HttpService httpService, Map<String, @Nullable Object> config) {
this.stateDescriptionProvider = stateDescriptionProvider;
this.timeZoneProvider = timeZoneProvider;
this.localeProvider = localeProvider;
this.translationProvider = translationProvider;
this.httpClient = factory.getCommonHttpClient();
this.httpService = httpService;
this.deserializer = deserializer;
configChanged(config);
}
@Override
protected void activate(ComponentContext componentContext) {
super.activate(componentContext);
Dictionary<String, Object> properties = componentContext.getProperties();
Object property = properties.get("backgroundDiscovery");
if (property instanceof Boolean) {
backgroundDiscovery = ((Boolean) property).booleanValue();
} else {
backgroundDiscovery = false;
@Modified
public void configChanged(Map<String, @Nullable Object> config) {
BindingConfiguration newConf = ConfigParser.configurationAs(config, BindingConfiguration.class);
if (newConf != null) {
configuration.update(newConf);
}
logger.debug("backgroundDiscovery {}", backgroundDiscovery);
}
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return (SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID));
return ModuleType.AS_SET.stream().anyMatch(mt -> mt.thingTypeUID.equals(thingTypeUID));
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (thingTypeUID.equals(APIBRIDGE_THING_TYPE)) {
WelcomeWebHookServlet servlet = registerWebHookServlet(thing.getUID());
NetatmoBridgeHandler bridgeHandler = new NetatmoBridgeHandler((Bridge) thing, servlet);
registerDeviceDiscoveryService(bridgeHandler);
return bridgeHandler;
} else if (thingTypeUID.equals(MODULE1_THING_TYPE)) {
return new NAModule1Handler(thing, timeZoneProvider);
} else if (thingTypeUID.equals(MODULE2_THING_TYPE)) {
return new NAModule2Handler(thing, timeZoneProvider);
} else if (thingTypeUID.equals(MODULE3_THING_TYPE)) {
return new NAModule3Handler(thing, timeZoneProvider);
} else if (thingTypeUID.equals(MODULE4_THING_TYPE)) {
return new NAModule4Handler(thing, timeZoneProvider);
} else if (thingTypeUID.equals(MAIN_THING_TYPE)) {
return new NAMainHandler(thing, timeZoneProvider);
} else if (thingTypeUID.equals(HOMECOACH_THING_TYPE)) {
return new NAHealthyHomeCoachHandler(thing, timeZoneProvider);
} else if (thingTypeUID.equals(PLUG_THING_TYPE)) {
return new NAPlugHandler(thing, timeZoneProvider);
} else if (thingTypeUID.equals(THERM1_THING_TYPE)) {
return new NATherm1Handler(thing, stateDescriptionProvider, timeZoneProvider);
} else if (thingTypeUID.equals(WELCOME_HOME_THING_TYPE)) {
return new NAWelcomeHomeHandler(thing, timeZoneProvider);
} else if (thingTypeUID.equals(WELCOME_CAMERA_THING_TYPE)) {
return new NAWelcomeCameraHandler(thing, timeZoneProvider);
} else if (thingTypeUID.equals(PRESENCE_CAMERA_THING_TYPE)) {
return new NAPresenceCameraHandler(thing, timeZoneProvider);
} else if (thingTypeUID.equals(WELCOME_PERSON_THING_TYPE)) {
return new NAWelcomePersonHandler(thing, timeZoneProvider);
} else {
logger.warn("ThingHandler not found for {}", thing.getThingTypeUID());
return null;
}
return ModuleType.AS_SET.stream().filter(mt -> mt.thingTypeUID.equals(thingTypeUID)).findFirst()
.map(mt -> buildHandler(thing, mt)).orElse(null);
}
@Override
protected void removeHandler(ThingHandler thingHandler) {
if (thingHandler instanceof NetatmoBridgeHandler) {
ThingUID thingUID = thingHandler.getThing().getUID();
unregisterDeviceDiscoveryService(thingUID);
unregisterWebHookServlet(thingUID);
private BaseThingHandler buildHandler(Thing thing, ModuleType moduleType) {
if (ModuleType.ACCOUNT.equals(moduleType)) {
return new ApiBridgeHandler((Bridge) thing, httpClient, httpService, deserializer, configuration);
}
}
CommonInterface handler = moduleType.isABridge() ? new DeviceHandler((Bridge) thing) : new ModuleHandler(thing);
private synchronized void registerDeviceDiscoveryService(NetatmoBridgeHandler netatmoBridgeHandler) {
if (bundleContext != null) {
NetatmoModuleDiscoveryService discoveryService = new NetatmoModuleDiscoveryService(netatmoBridgeHandler,
localeProvider, translationProvider);
Map<String, Object> configProperties = new HashMap<>();
configProperties.put(DiscoveryService.CONFIG_PROPERTY_BACKGROUND_DISCOVERY,
Boolean.valueOf(backgroundDiscovery));
discoveryService.activate(configProperties);
discoveryServiceRegs.put(netatmoBridgeHandler.getThing().getUID(), bundleContext
.registerService(DiscoveryService.class.getName(), discoveryService, new Hashtable<>()));
}
}
private synchronized void unregisterDeviceDiscoveryService(ThingUID thingUID) {
ServiceRegistration<?> serviceReg = discoveryServiceRegs.remove(thingUID);
if (serviceReg != null) {
NetatmoModuleDiscoveryService service = (NetatmoModuleDiscoveryService) bundleContext
.getService(serviceReg.getReference());
serviceReg.unregister();
if (service != null) {
service.deactivate();
List<ChannelHelper> helpers = new ArrayList<>();
moduleType.channelHelpers.forEach(helperClass -> {
try {
helpers.add(helperClass.getConstructor().newInstance());
} catch (ReflectiveOperationException e) {
logger.warn("Error creating or initializing helper class : {}", e.getMessage());
}
}
}
});
private synchronized @Nullable WelcomeWebHookServlet registerWebHookServlet(ThingUID thingUID) {
WelcomeWebHookServlet servlet = null;
if (bundleContext != null) {
servlet = new WelcomeWebHookServlet(httpService, thingUID.getId());
webHookServiceRegs.put(thingUID,
bundleContext.registerService(HttpServlet.class.getName(), servlet, new Hashtable<>()));
}
return servlet;
}
moduleType.capabilities.forEach(capability -> {
Capability newCap = null;
if (capability == DeviceCapability.class) {
newCap = new DeviceCapability(handler);
} else if (capability == AirCareCapability.class) {
newCap = new AirCareCapability(handler);
} else if (capability == EventCapability.class) {
newCap = new EventCapability(handler);
} else if (capability == HomeCapability.class) {
newCap = new HomeCapability(handler, stateDescriptionProvider);
} else if (capability == WeatherCapability.class) {
newCap = new WeatherCapability(handler);
} else if (capability == RoomCapability.class) {
newCap = new RoomCapability(handler);
} else if (capability == PersonCapability.class) {
newCap = new PersonCapability(handler, stateDescriptionProvider, helpers);
} else if (capability == CameraCapability.class) {
newCap = new CameraCapability(handler, stateDescriptionProvider, helpers);
} else if (capability == PresenceCapability.class) {
newCap = new PresenceCapability(handler, stateDescriptionProvider, helpers);
} else if (capability == MeasureCapability.class) {
newCap = new MeasureCapability(handler, helpers);
} else if (capability == ChannelHelperCapability.class) {
newCap = new ChannelHelperCapability(handler, helpers);
}
if (newCap != null) {
handler.getCapabilities().put(newCap);
} else {
logger.warn("No factory entry defined to create Capability : {}", capability);
}
});
private synchronized void unregisterWebHookServlet(ThingUID thingUID) {
ServiceRegistration<?> serviceReg = webHookServiceRegs.remove(thingUID);
if (serviceReg != null) {
serviceReg.unregister();
}
return (BaseThingHandler) handler;
}
}

View File

@ -1,99 +0,0 @@
/**
* 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;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import java.util.Calendar;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* {@link RefreshStrategy} is the class used to embed the refreshing
* needs calculation for devices
*
* @author Gaël L'hopital - Initial contribution
*
*/
@NonNullByDefault
public class RefreshStrategy {
private Logger logger = LoggerFactory.getLogger(RefreshStrategy.class);
private static final int DEFAULT_DELAY = 30; // in seconds
private static final int SEARCH_REFRESH_INTERVAL = 120; // in seconds
private int dataValidityPeriod;
private long dataTimeStamp;
private boolean searchRefreshInterval;
@Nullable
private Integer dataTimestamp0;
// By default we create dataTimeStamp to be outdated
// A null or negative value for dataValidityPeriod will trigger an automatic search of the validity period
public RefreshStrategy(int dataValidityPeriod) {
if (dataValidityPeriod <= 0) {
this.dataValidityPeriod = 0;
this.searchRefreshInterval = true;
logger.debug("Data validity period search...");
} else {
this.dataValidityPeriod = dataValidityPeriod;
this.searchRefreshInterval = false;
logger.debug("Data validity period set to {} ms", this.dataValidityPeriod);
}
expireData();
}
@SuppressWarnings("null")
public void setDataTimeStamp(Integer dataTimestamp, ZoneId zoneId) {
if (searchRefreshInterval) {
if (dataTimestamp0 == null) {
dataTimestamp0 = dataTimestamp;
logger.debug("First data timestamp is {}", dataTimestamp0);
} else if (dataTimestamp.intValue() > dataTimestamp0.intValue()) {
dataValidityPeriod = (dataTimestamp.intValue() - dataTimestamp0.intValue()) * 1000;
searchRefreshInterval = false;
logger.debug("Data validity period found : {} ms", this.dataValidityPeriod);
} else {
logger.debug("Data validity period not yet found - data timestamp unchanged");
}
}
this.dataTimeStamp = ChannelTypeUtils.toZonedDateTime(dataTimestamp, zoneId).toInstant().toEpochMilli();
}
public long dataAge() {
long now = Calendar.getInstance().getTimeInMillis();
return now - dataTimeStamp;
}
public boolean isDataOutdated() {
return dataAge() >= dataValidityPeriod;
}
public long nextRunDelayInS() {
return searchRefreshInterval ? SEARCH_REFRESH_INTERVAL
: Math.max(0, (dataValidityPeriod - dataAge())) / 1000 + DEFAULT_DELAY;
}
public void expireData() {
ZonedDateTime now = ZonedDateTime.now().minus(this.dataValidityPeriod, ChronoUnit.MILLIS);
dataTimeStamp = now.toInstant().toEpochMilli();
}
public boolean isSearchingRefreshInterval() {
return searchRefreshInterval && dataTimestamp0 != null;
}
}

View File

@ -0,0 +1,150 @@
/**
* 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.action;
import java.util.Optional;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.netatmo.internal.api.data.NetatmoConstants.SetpointMode;
import org.openhab.binding.netatmo.internal.handler.CommonInterface;
import org.openhab.binding.netatmo.internal.handler.capability.EnergyCapability;
import org.openhab.core.automation.annotation.ActionInput;
import org.openhab.core.automation.annotation.RuleAction;
import org.openhab.core.thing.binding.ThingActions;
import org.openhab.core.thing.binding.ThingActionsScope;
import org.openhab.core.thing.binding.ThingHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link RoomActions} defines thing actions for RoomHandler.
*
* @author Markus Dillmann - Initial contribution
*/
@ThingActionsScope(name = "netatmo")
@NonNullByDefault
public class RoomActions implements ThingActions {
private final Logger logger = LoggerFactory.getLogger(RoomActions.class);
private static final Set<SetpointMode> ALLOWED_MODES = Set.of(SetpointMode.MAX, SetpointMode.MANUAL,
SetpointMode.HOME);
private @Nullable CommonInterface handler;
private Optional<EnergyCapability> energy = Optional.empty();
public RoomActions() {
logger.debug("Netatmo RoomActions service created");
}
@Override
public void setThingHandler(@Nullable ThingHandler handler) {
if (handler instanceof CommonInterface) {
CommonInterface commonHandler = (CommonInterface) handler;
this.handler = commonHandler;
energy = commonHandler.getHomeCapability(EnergyCapability.class);
}
}
@Override
public @Nullable ThingHandler getThingHandler() {
return (ThingHandler) handler;
}
/**
* The setThermpoint room thing action
*/
@RuleAction(label = "@text/actionLabel", description = "@text/actionDesc")
public void setThermpoint(
@ActionInput(name = "setpoint", label = "@text/actionInputSetpointLabel", description = "@text/actionInputSetpointDesc") @Nullable Double temp,
@ActionInput(name = "endtime", label = "@text/actionInputEndtimeLabel", description = "@text/actionInputEndtimeDesc") @Nullable Long endTime) {
setThermpoint(temp, endTime, "MANUAL");
}
@RuleAction(label = "@text/actionLabel", description = "@text/actionDesc")
public void seThermpoint(
@ActionInput(name = "mode", label = "@text/actionInputModeLabel", description = "@text/actionInputModeDesc") @Nullable String mode,
@ActionInput(name = "endtime", label = "@text/actionInputEndtimeLabel", description = "@text/actionInputEndtimeDesc") @Nullable Long endTime) {
setThermpoint(null, endTime, mode);
}
@RuleAction(label = "@text/actionLabel", description = "@text/actionDesc")
public void setThermpoint(
@ActionInput(name = "setpoint", label = "@text/actionInputSetpointLabel", description = "@text/actionInputSetpointDesc") @Nullable Double temp,
@ActionInput(name = "endtime", label = "@text/actionInputEndtimeLabel", description = "@text/actionInputEndtimeDesc") @Nullable Long endTime,
@ActionInput(name = "mode", label = "@text/actionInputModeLabel", description = "@text/actionInputModeDesc") @Nullable String mode) {
CommonInterface roomHandler = handler;
if (roomHandler != null) {
String roomId = roomHandler.getId();
SetpointMode targetMode = SetpointMode.UNKNOWN;
Long targetEndTime = endTime;
Double targetTemp = temp;
if (mode != null) {
try {
targetMode = SetpointMode.valueOf(mode);
if (!ALLOWED_MODES.contains(targetMode)) {
logger.info("Mode can only be MAX, HOME or MANUAL for a room");
return;
}
} catch (IllegalArgumentException e) {
logger.info("Invalid mode passed : {} - {}", mode, e.getMessage());
return;
}
}
if (temp != null) {
logger.debug("Temperature provided, mode forced to MANUAL.");
targetMode = SetpointMode.MANUAL;
if (targetEndTime == null) {
logger.info("Temperature provided but no endtime given, action ignored");
return;
}
} else {
if (SetpointMode.HOME.equals(targetMode)) {
targetEndTime = 0L;
targetTemp = 0.0;
} else {
logger.info("mode is required if no temperature setpoint provided");
return;
}
}
try {
double setpointTemp = targetTemp != null ? targetTemp : 0;
long setpointEnd = targetEndTime;
SetpointMode setpointMode = targetMode;
energy.ifPresent(cap -> cap.setRoomThermTemp(roomId, setpointTemp, setpointEnd, setpointMode));
} catch (IllegalArgumentException e) {
logger.debug("Ignoring setRoomThermpoint command due to illegal argument exception: {}",
e.getMessage());
}
} else {
logger.info("Handler not set for room thing actions.");
}
}
/**
* Static setThermpoint method for Rules DSL backward compatibility
*/
public static void setThermpoint(ThingActions actions, @Nullable Double temp, @Nullable Long endTime,
@Nullable String mode) {
((RoomActions) actions).setThermpoint(temp, endTime, mode);
}
public static void setThermpoint(ThingActions actions, @Nullable Double temp, @Nullable Long endTime) {
setThermpoint(actions, temp, endTime, null);
}
public static void setThermpoint(ThingActions actions, @Nullable String mode, @Nullable Long endTime) {
setThermpoint(actions, null, endTime, mode);
}
}

View File

@ -0,0 +1,60 @@
/**
* 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.api;
import static org.openhab.binding.netatmo.internal.api.data.NetatmoConstants.*;
import javax.ws.rs.core.UriBuilder;
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.dto.NAMain;
import org.openhab.binding.netatmo.internal.api.dto.NAMain.StationDataResponse;
import org.openhab.binding.netatmo.internal.handler.ApiBridgeHandler;
/**
* Base class for all Air Care related endpoints
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class AircareApi extends RestManager {
public AircareApi(ApiBridgeHandler apiClient) {
super(apiClient, FeatureArea.AIR_CARE);
}
/**
* Returns data from Healthy Home Coach Station (measures and device specific data).
*
* @param deviceId Id of the device you want to retrieve information of (optional)
* @return StationDataResponse
* @throws NetatmoException If fail to call the API, e.g. server error or deserializing
*/
public StationDataResponse getHomeCoachData(@Nullable String deviceId) throws NetatmoException {
UriBuilder uriBuilder = getApiUriBuilder(SUB_PATH_HOMECOACH, PARAM_DEVICEID, deviceId);
return get(uriBuilder, StationDataResponse.class);
}
public NAMain getHomeCoach(String deviceId) throws NetatmoException {
ListBodyResponse<NAMain> answer = getHomeCoachData(deviceId).getBody();
if (answer != null) {
NAMain station = answer.getElement(deviceId);
if (station != null) {
return station;
}
}
throw new NetatmoException("Unexpected answer querying device '%s' : not found.", deviceId);
}
}

View File

@ -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.api;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.netatmo.internal.api.data.NetatmoConstants.ServiceError;
/**
* The {@link ApiError} models an errored response from API
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class ApiError {
private class Body {
private String message = "";
private ServiceError code = ServiceError.UNKNOWN;
}
private Body error = new Body();
public String getMessage() {
return error.message;
}
public ServiceError getCode() {
return error.code;
}
}

View File

@ -0,0 +1,46 @@
/**
* 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.api;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link ApiResponse} models a response returned by API call
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class ApiResponse<T> {
/**
* The {@link Ok} models a response that only holds the result of the request sent to the API
*/
static class Ok extends ApiResponse<String> {
private static final String SUCCESS = "ok";
boolean failed() {
return !SUCCESS.equals(getStatus());
}
}
private String status = "";
private @Nullable T body;
public String getStatus() {
return status;
}
public @Nullable T getBody() {
return body;
}
}

View File

@ -0,0 +1,106 @@
/**
* 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.api;
import static org.openhab.binding.netatmo.internal.api.data.NetatmoConstants.PATH_OAUTH;
import static org.openhab.core.auth.oauth2client.internal.Keyword.*;
import java.net.URI;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
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.Scope;
import org.openhab.binding.netatmo.internal.api.dto.AccessTokenResponse;
import org.openhab.binding.netatmo.internal.config.ApiHandlerConfiguration.Credentials;
import org.openhab.binding.netatmo.internal.handler.ApiBridgeHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link AuthenticationApi} handles oAuth2 authentication and token refreshing
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class AuthenticationApi extends RestManager {
private static final URI OAUTH_URI = getApiBaseBuilder().path(PATH_OAUTH).build();
private final ScheduledExecutorService scheduler;
private final Logger logger = LoggerFactory.getLogger(AuthenticationApi.class);
private @Nullable ScheduledFuture<?> refreshTokenJob;
private Optional<AccessTokenResponse> tokenResponse = Optional.empty();
private String scope = "";
public AuthenticationApi(ApiBridgeHandler bridge, ScheduledExecutorService scheduler) {
super(bridge, FeatureArea.NONE);
this.scheduler = scheduler;
}
public void authenticate(Credentials credentials, Set<FeatureArea> features) throws NetatmoException {
Set<FeatureArea> requestedFeatures = !features.isEmpty() ? features : FeatureArea.AS_SET;
scope = FeatureArea.toScopeString(requestedFeatures);
requestToken(credentials.clientId, credentials.clientSecret,
Map.of(USERNAME, credentials.username, PASSWORD, credentials.password, SCOPE, scope));
}
private void requestToken(String id, String secret, Map<String, String> entries) throws NetatmoException {
Map<String, String> payload = new HashMap<>(entries);
payload.putAll(Map.of(GRANT_TYPE, entries.keySet().contains(PASSWORD) ? PASSWORD : REFRESH_TOKEN, CLIENT_ID, id,
CLIENT_SECRET, secret));
disconnect();
AccessTokenResponse response = post(OAUTH_URI, AccessTokenResponse.class, payload);
refreshTokenJob = scheduler.schedule(() -> {
try {
requestToken(id, secret, Map.of(REFRESH_TOKEN, response.getRefreshToken()));
} catch (NetatmoException e) {
logger.warn("Unable to refresh access token : {}", e.getMessage());
}
}, Math.round(response.getExpiresIn() * 0.8), TimeUnit.SECONDS);
tokenResponse = Optional.of(response);
}
public void disconnect() {
tokenResponse = Optional.empty();
}
public void dispose() {
ScheduledFuture<?> job = refreshTokenJob;
if (job != null) {
job.cancel(true);
}
refreshTokenJob = null;
}
public @Nullable String getAuthorization() {
return tokenResponse.map(at -> String.format("Bearer %s", at.getAccessToken())).orElse(null);
}
public boolean matchesScopes(Set<Scope> requiredScopes) {
// either we do not require any scope, either connected and all scopes available
return requiredScopes.isEmpty()
|| (isConnected() && tokenResponse.map(at -> at.getScope().containsAll(requiredScopes)).orElse(false));
}
public boolean isConnected() {
return !tokenResponse.isEmpty();
}
}

View File

@ -0,0 +1,36 @@
/**
* 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.api;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.netatmo.internal.api.dto.NAObject;
import com.google.gson.annotations.SerializedName;
/**
* The {@link BodyResponse} models a response returned by API call containing
* a list of elements.
*
* @author Gaël L'hopital - Initial contribution
*
*/
@NonNullByDefault
public class BodyResponse<T extends NAObject> {
@SerializedName(value = "home")
private @Nullable T element;
public @Nullable T getElement() {
return element;
}
}

View File

@ -0,0 +1,93 @@
/**
* 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.api;
import static org.openhab.binding.netatmo.internal.api.data.NetatmoConstants.*;
import javax.ws.rs.core.UriBuilder;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.netatmo.internal.api.data.NetatmoConstants.FeatureArea;
import org.openhab.binding.netatmo.internal.api.data.NetatmoConstants.SetpointMode;
import org.openhab.binding.netatmo.internal.handler.ApiBridgeHandler;
/**
* The {@link EnergyApi} handles API endpoints related to Energy feature area
*
* @author Gaël L'hopital - Initial contribution
*
*/
@NonNullByDefault
public class EnergyApi extends RestManager {
public EnergyApi(ApiBridgeHandler apiClient) {
super(apiClient, FeatureArea.ENERGY);
}
/**
*
* The method switchSchedule switches the home's schedule to another existing schedule.
*
* @param homeId The id of home (required)
* @param scheduleId The schedule id. It can be found in the getthermstate response, under the keys
* therm_program_backup and therm_program. (required)
* @throws NetatmoException If fail to call the API, e.g. server error or cannot deserialize the
* response body
*/
public void switchSchedule(String homeId, String scheduleId) throws NetatmoException {
UriBuilder uriBuilder = getAppUriBuilder(SUB_PATH_SWITCHSCHEDULE, PARAM_HOMEID, homeId, PARAM_SCHEDULEID,
scheduleId);
post(uriBuilder, ApiResponse.Ok.class, null, null);
}
/**
*
* This endpoint permits to control the heating of a specific home. A home can be set in 3 differents modes:
* "schedule" mode in which the home will follow the user schedule
* "away" mode which will put the whole house to away (default is 12° but can be changed by the user in its
* settings)
* "hg" corresponds to frostguard mode (7° by default)
*
* @param homeId The id of home (required)
* @param mode The mode. (required)
* @throws NetatmoCommunicationException when call failed, e.g. server error or cannot deserialize
*/
public void setThermMode(String homeId, String mode) throws NetatmoException {
UriBuilder uriBuilder = getApiUriBuilder(SUB_PATH_SETTHERMMODE, PARAM_HOMEID, homeId, PARAM_MODE, mode);
post(uriBuilder, ApiResponse.Ok.class, null, null);
}
/**
*
* The method setThermpoint changes the Thermostat manual temperature setpoint.
*
* @param homeId The id of home (required)
* @param roomId The id of the room (required)
* @param mode The mode. (required)
* @param endtime For manual or max setpoint_mode, defines when the setpoint expires.
* @param temp For manual setpoint_mode, defines the temperature setpoint (in °C)
* @throws NetatmoCommunicationException when call failed, e.g. server error or cannot deserialize
*/
public void setThermpoint(String homeId, String roomId, SetpointMode mode, long endtime, double temp)
throws NetatmoException {
UriBuilder uriBuilder = getApiUriBuilder(SUB_PATH_SETROOMTHERMPOINT, PARAM_HOMEID, homeId, PARAM_ROOMID, roomId,
PARAM_MODE, mode.apiDescriptor);
if (mode == SetpointMode.MANUAL || mode == SetpointMode.MAX) {
uriBuilder.queryParam("endtime", endtime);
if (mode == SetpointMode.MANUAL) {
uriBuilder.queryParam("temp", temp > THERM_MAX_SETPOINT ? THERM_MAX_SETPOINT : temp);
}
}
post(uriBuilder, ApiResponse.Ok.class, null, null);
}
}

View File

@ -0,0 +1,71 @@
/**
* 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.api;
import static org.openhab.binding.netatmo.internal.api.data.NetatmoConstants.*;
import java.util.Collection;
import java.util.Set;
import javax.ws.rs.core.UriBuilder;
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.data.NetatmoConstants.FeatureArea;
import org.openhab.binding.netatmo.internal.api.dto.HomeData;
import org.openhab.binding.netatmo.internal.api.dto.NAHomeStatus;
import org.openhab.binding.netatmo.internal.api.dto.NAHomeStatus.HomeStatus;
import org.openhab.binding.netatmo.internal.api.dto.NAHomeStatus.NAHomeStatusResponse;
import org.openhab.binding.netatmo.internal.handler.ApiBridgeHandler;
/**
* The {@link HomeApi} handles general API endpoints not requiring specific scope area
*
* @author Gaël L'hopital - Initial contribution
*
*/
@NonNullByDefault
public class HomeApi extends RestManager {
public HomeApi(ApiBridgeHandler apiClient) {
super(apiClient, FeatureArea.NONE);
}
public @Nullable HomeStatus getHomeStatus(String homeId) throws NetatmoException {
UriBuilder uriBuilder = getApiUriBuilder(SUB_PATH_HOMESTATUS, PARAM_HOMEID, homeId);
NAHomeStatusResponse response = get(uriBuilder, NAHomeStatusResponse.class);
NAHomeStatus body = response.getBody();
return body != null ? body.getHomeStatus().orElse(null) : null;
}
public @Nullable HomeData getHomeData(String homeId) throws NetatmoException {
Collection<HomeData> result = getHomesData(homeId, null);
return result.isEmpty() ? null : result.iterator().next();
}
public Collection<HomeData> getHomesData(@Nullable String homeId, @Nullable ModuleType type)
throws NetatmoException {
UriBuilder uriBuilder = getApiUriBuilder(SUB_PATH_HOMES_DATA, PARAM_HOMEID, homeId);
if (type != null) {
uriBuilder.queryParam(PARAM_GATEWAYTYPE, type.name());
}
HomeData.HomesDataResponse response = get(uriBuilder, HomeData.HomesDataResponse.class);
ListBodyResponse<HomeData> body = response.getBody();
return body != null ? body.getElements() : Set.of();
}
}

View File

@ -0,0 +1,44 @@
/**
* 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.api;
import java.util.Collection;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.netatmo.internal.api.dto.NAObject;
import org.openhab.binding.netatmo.internal.deserialization.NAObjectMap;
import com.google.gson.annotations.SerializedName;
/**
* The {@link ListBodyResponse} models a response returned by API call containing
* a list of elements.
*
* @author Gaël L'hopital - Initial contribution
*
*/
@NonNullByDefault
public class ListBodyResponse<T extends NAObject> {
@SerializedName(value = "devices", alternate = { "homes", "events_list", "events" })
private NAObjectMap<T> elements = new NAObjectMap<>();
@Nullable
T getElement(String id) {
return elements.get(id);
}
public Collection<T> getElements() {
return elements.values();
}
}

View File

@ -0,0 +1,58 @@
/**
* 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.api;
import java.io.IOException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.netatmo.internal.api.data.NetatmoConstants.ServiceError;
/**
* An exception that occurred while communicating with Netatmo server or related processes.
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class NetatmoException extends IOException {
private static final long serialVersionUID = 1513549973502021727L;
private ServiceError statusCode = ServiceError.UNKNOWN;
public NetatmoException(String format, Object... args) {
super(String.format(format, args));
}
public NetatmoException(Exception e, String format, Object... args) {
super(String.format(format, args), e);
}
public NetatmoException(String message) {
super(message);
}
public NetatmoException(ApiError error) {
super(error.getMessage());
this.statusCode = error.getCode();
}
public ServiceError getStatusCode() {
return statusCode;
}
@Override
public @Nullable String getMessage() {
String message = super.getMessage();
return message == null ? null
: String.format("Rest call failed: statusCode=%s, message=%s", statusCode, message);
}
}

View File

@ -0,0 +1,112 @@
/**
* 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.api;
import static org.eclipse.jetty.http.HttpMethod.POST;
import static org.openhab.binding.netatmo.internal.api.data.NetatmoConstants.*;
import java.net.URI;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import javax.ws.rs.core.UriBuilder;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.http.HttpMethod;
import org.openhab.binding.netatmo.internal.api.data.NetatmoConstants.FeatureArea;
import org.openhab.binding.netatmo.internal.api.data.NetatmoConstants.Scope;
import org.openhab.binding.netatmo.internal.handler.ApiBridgeHandler;
/**
* Base class for all various rest managers
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public abstract class RestManager {
private static final UriBuilder API_BASE_BUILDER = UriBuilder.fromUri(URL_API);
private static final UriBuilder APP_URI_BUILDER = UriBuilder.fromUri(URL_APP).path(PATH_API);
private static final UriBuilder API_URI_BUILDER = getApiBaseBuilder().path(PATH_API);
private final Set<Scope> requiredScopes;
private final ApiBridgeHandler apiBridge;
public RestManager(ApiBridgeHandler apiBridge, FeatureArea features) {
this.requiredScopes = features.scopes;
this.apiBridge = apiBridge;
}
protected <T extends ApiResponse<?>> T get(UriBuilder uriBuilder, Class<T> clazz) throws NetatmoException {
return executeUri(uriBuilder, HttpMethod.GET, clazz, null, null);
}
protected <T extends ApiResponse<?>> T post(UriBuilder uriBuilder, Class<T> clazz, @Nullable String payload,
@Nullable String contentType) throws NetatmoException {
return executeUri(uriBuilder, HttpMethod.POST, clazz, payload, contentType);
}
protected <T> T post(URI uri, Class<T> clazz, Map<String, String> entries) throws NetatmoException {
return apiBridge.executeUri(uri, POST, clazz, toRequest(entries),
"application/x-www-form-urlencoded;charset=UTF-8", 3);
}
private <T extends ApiResponse<?>> T executeUri(UriBuilder uriBuilder, HttpMethod method, Class<T> clazz,
@Nullable String payload, @Nullable String contentType) throws NetatmoException {
URI uri = uriBuilder.build();
T response = apiBridge.executeUri(uri, method, clazz, payload, contentType, 3);
if (response instanceof ApiResponse.Ok && ((ApiResponse.Ok) response).failed()) {
throw new NetatmoException("Command failed : %s for uri : %s", response.getStatus(), uri.toString());
}
return response;
}
private static UriBuilder appendParams(UriBuilder builder, @Nullable Object... params) {
if (params.length % 2 != 0) {
throw new IllegalArgumentException("appendParams : params count must be even");
}
for (int i = 0; i < params.length; i += 2) {
Object query = params[i];
if (query instanceof String) {
Object param = params[i + 1];
if (param != null) {
builder.queryParam((String) query, param);
}
} else {
throw new IllegalArgumentException("appendParams : even parameters must be Strings");
}
}
return builder;
}
protected static UriBuilder getApiBaseBuilder() {
return API_BASE_BUILDER.clone();
}
public static UriBuilder getApiUriBuilder(String path, @Nullable Object... params) {
return appendParams(API_URI_BUILDER.clone().path(path), params);
}
protected static UriBuilder getAppUriBuilder(String path, @Nullable Object... params) {
return appendParams(APP_URI_BUILDER.clone().path(path), params);
}
private String toRequest(Map<String, String> entries) {
return entries.entrySet().stream().map(e -> e.getKey() + "=" + e.getValue()).collect(Collectors.joining("&"));
}
public Set<Scope> getRequiredScopes() {
return requiredScopes;
}
}

View File

@ -0,0 +1,117 @@
/**
* 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.api;
import static org.openhab.binding.netatmo.internal.api.data.NetatmoConstants.*;
import java.net.URI;
import java.util.Collection;
import java.util.stream.Collectors;
import javax.ws.rs.core.UriBuilder;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.netatmo.internal.api.data.NetatmoConstants.FeatureArea;
import org.openhab.binding.netatmo.internal.api.data.NetatmoConstants.FloodLightMode;
import org.openhab.binding.netatmo.internal.api.dto.Home;
import org.openhab.binding.netatmo.internal.api.dto.HomeEvent;
import org.openhab.binding.netatmo.internal.api.dto.HomeEvent.NAEventsDataResponse;
import org.openhab.binding.netatmo.internal.api.dto.Ping;
import org.openhab.binding.netatmo.internal.handler.ApiBridgeHandler;
/**
* Base class for all Security related endpoints
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class SecurityApi extends RestManager {
public SecurityApi(ApiBridgeHandler apiClient) {
super(apiClient, FeatureArea.SECURITY);
}
/**
* Dissociates a webhook from a user.
*
* @throws NetatmoException If fail to call the API, e.g. server error or deserializing
*/
public void dropWebhook() throws NetatmoException {
UriBuilder uriBuilder = getApiUriBuilder(SUB_PATH_DROPWEBHOOK);
post(uriBuilder, ApiResponse.Ok.class, null, null);
}
/**
* Links a callback url to a user.
*
* @param uri Your webhook callback url (required)
* @throws NetatmoException If fail to call the API, e.g. server error or deserializing
*/
public void addwebhook(URI uri) throws NetatmoException {
UriBuilder uriBuilder = getApiUriBuilder(SUB_PATH_ADDWEBHOOK, PARAM_URL, uri.toString());
post(uriBuilder, ApiResponse.Ok.class, null, null);
}
public Collection<HomeEvent> getPersonEvents(String homeId, String personId) throws NetatmoException {
UriBuilder uriBuilder = getApiUriBuilder(SUB_PATH_GETEVENTS, PARAM_HOMEID, homeId, PARAM_PERSONID, personId,
PARAM_OFFSET, 1);
NAEventsDataResponse response = get(uriBuilder, NAEventsDataResponse.class);
BodyResponse<Home> body = response.getBody();
if (body != null) {
Home home = body.getElement();
if (home != null) {
return home.getEvents().stream().filter(event -> personId.equals(event.getPersonId()))
.collect(Collectors.toList());
}
}
throw new NetatmoException("home should not be null");
}
public Collection<HomeEvent> getCameraEvents(String homeId, String cameraId) throws NetatmoException {
UriBuilder uriBuilder = getApiUriBuilder(SUB_PATH_GETEVENTS, PARAM_HOMEID, homeId, PARAM_DEVICEID, cameraId);
NAEventsDataResponse response = get(uriBuilder, NAEventsDataResponse.class);
BodyResponse<Home> body = response.getBody();
if (body != null) {
Home home = body.getElement();
if (home != null) {
return home.getEvents();
}
}
throw new NetatmoException("home should not be null");
}
public String ping(String vpnUrl) throws NetatmoException {
UriBuilder uriBuilder = UriBuilder.fromUri(vpnUrl).path(PATH_COMMAND).path(SUB_PATH_PING);
Ping response = get(uriBuilder, Ping.class);
return response.getStatus();
}
public void changeStatus(String localCameraURL, boolean setOn) throws NetatmoException {
UriBuilder uriBuilder = UriBuilder.fromUri(localCameraURL).path(PATH_COMMAND).path(SUB_PATH_CHANGESTATUS);
uriBuilder.queryParam(PARAM_STATUS, setOn ? "on" : "off");
post(uriBuilder, ApiResponse.Ok.class, null, null);
}
public void changeFloodLightMode(String localCameraURL, FloodLightMode mode) throws NetatmoException {
UriBuilder uriBuilder = UriBuilder.fromUri(localCameraURL).path(PATH_COMMAND).path(SUB_PATH_FLOODLIGHTSET);
uriBuilder.queryParam("config", "%7B%22mode%22:%22" + mode.toString() + "%22%7D");
get(uriBuilder, ApiResponse.Ok.class);
}
public void setPersonAwayStatus(String homeId, String personId, boolean away) throws NetatmoException {
UriBuilder uriBuilder = getAppUriBuilder(away ? SUB_PATH_PERSON_AWAY : SUB_PATH_PERSON_HOME);
String payload = String.format(
away ? "{\"home_id\":\"%s\",\"person_id\":\"%s\"}" : "{\"home_id\":\"%s\",\"person_ids\":[\"%s\"]}",
homeId, personId);
post(uriBuilder, ApiResponse.Ok.class, payload, "application/json;charset=utf-8");
}
}

View File

@ -0,0 +1,117 @@
/**
* 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.api;
import static org.openhab.binding.netatmo.internal.api.data.NetatmoConstants.*;
import java.time.ZonedDateTime;
import java.util.List;
import javax.ws.rs.core.UriBuilder;
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.dto.MeasureBodyElem;
import org.openhab.binding.netatmo.internal.api.dto.NAMain;
import org.openhab.binding.netatmo.internal.api.dto.NAMain.StationDataResponse;
import org.openhab.binding.netatmo.internal.handler.ApiBridgeHandler;
/**
* Base class for all Weather related endpoints
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class WeatherApi extends RestManager {
private class NAMeasuresResponse extends ApiResponse<List<MeasureBodyElem<Double>>> {
}
private class NADateMeasuresResponse extends ApiResponse<List<MeasureBodyElem<ZonedDateTime>>> {
}
public WeatherApi(ApiBridgeHandler apiClient) {
super(apiClient, FeatureArea.WEATHER);
}
/**
*
* Returns data from a user's Weather Stations (measures and device specific data);
*
* @param deviceId Id of the device you want to retrieve information of (optional)
* @param getFavorites Whether to include the user's favorite Weather Stations in addition to the user's
* own Weather Stations (optional, default to false)
* @return StationDataResponse
* @throws NetatmoException If fail to call the API, e.g. server error or deserializing
*/
public StationDataResponse getStationsData(@Nullable String deviceId, boolean getFavorites)
throws NetatmoException {
UriBuilder uriBuilder = getApiUriBuilder(SUB_PATH_GETSTATION, PARAM_DEVICEID, deviceId, //
PARAM_FAVORITES, getFavorites);
StationDataResponse response = get(uriBuilder, StationDataResponse.class);
return response;
}
public NAMain getStationData(String deviceId) throws NetatmoException {
ListBodyResponse<NAMain> answer = getStationsData(deviceId, true).getBody();
if (answer != null) {
NAMain station = answer.getElement(deviceId);
if (station != null) {
return station;
}
}
throw new NetatmoException("Unexpected answer searching device '%s' : not found.", deviceId);
}
public @Nullable Object getMeasures(String deviceId, @Nullable String moduleId, @Nullable String scale,
String apiDescriptor) throws NetatmoException {
MeasureBodyElem<?> result = getMeasure(deviceId, moduleId, scale, apiDescriptor);
return result.getSingleValue();
}
public @Nullable Object getMeasures(String deviceId, @Nullable String moduleId, @Nullable String scale,
String apiDescriptor, String limit) throws NetatmoException {
String queryLimit = limit;
if (!apiDescriptor.contains("_")) {
queryLimit += "_" + apiDescriptor;
}
MeasureBodyElem<?> result = getMeasure(deviceId, moduleId, scale, queryLimit.toLowerCase());
return result.getSingleValue();
}
private MeasureBodyElem<?> getMeasure(String deviceId, @Nullable String moduleId, @Nullable String scale,
String measureType) throws NetatmoException {
// NAMeasuresResponse is not designed for optimize=false
UriBuilder uriBuilder = getApiUriBuilder(SUB_PATH_GETMEASURE, PARAM_DEVICEID, deviceId, "real_time", true,
"date_end", "last", "optimize", true, "type", measureType.toLowerCase(), PARAM_MODULEID, moduleId);
if (scale != null) {
uriBuilder.queryParam("scale", scale.toLowerCase());
}
if (measureType.startsWith("date")) {
NADateMeasuresResponse response = get(uriBuilder, NADateMeasuresResponse.class);
List<MeasureBodyElem<ZonedDateTime>> body = response.getBody();
if (body != null && !body.isEmpty()) {
return body.get(0);
}
} else {
NAMeasuresResponse response = get(uriBuilder, NAMeasuresResponse.class);
List<MeasureBodyElem<Double>> body = response.getBody();
if (body != null && !body.isEmpty()) {
return body.get(0);
}
}
throw new NetatmoException("Empty response while getting measurements");
}
}

View File

@ -0,0 +1,51 @@
/**
* 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.api.data;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* This enum describes sub events in relation to a given event
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public enum EventSubType {
SD_CARD_MISSING(List.of(EventType.SD), 1),
SD_CARD_INSERTED(List.of(EventType.SD), 2),
SD_CARD_FORMATTED(List.of(EventType.SD), 3),
SD_CARD_WORKING(List.of(EventType.SD), 4),
SD_CARD_DEFECTIVE(List.of(EventType.SD), 5),
SD_CARD_INCOMPATIBLE_SPEED(List.of(EventType.SD), 6),
SD_CARD_INSUFFICIENT_SPACE(List.of(EventType.SD), 7),
ALIM_INCORRECT_POWER(List.of(EventType.ALIM), 1),
ALIM_CORRECT_POWER(List.of(EventType.ALIM), 2),
// Artificially implemented by the binding subtypes
PERSON_ARRIVAL(List.of(EventType.PERSON, EventType.PERSON_HOME), 1),
PERSON_SEEN(List.of(EventType.PERSON), 2),
PERSON_DEPARTURE(List.of(EventType.PERSON_AWAY), 1),
MOVEMENT_HUMAN(List.of(EventType.MOVEMENT, EventType.HUMAN), 1),
MOVEMENT_VEHICLE(List.of(EventType.MOVEMENT), 2),
MOVEMENT_ANIMAL(List.of(EventType.MOVEMENT, EventType.ANIMAL), 3);
public final List<EventType> types;
public final int subType;
EventSubType(List<EventType> types, int i) {
this.types = types;
this.subType = i;
}
}

View File

@ -0,0 +1,105 @@
/**
* 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.api.data;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.annotations.SerializedName;
/**
* This enum describes events generated by webhooks and the type of
* module they are related to according to API documentation
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public enum EventType {
UNKNOWN(),
@SerializedName("person") // When the Indoor Camera detects a face
PERSON(ModuleType.PERSON, ModuleType.WELCOME),
@SerializedName("person_away") // When geofencing indicates that the person has left the home
PERSON_AWAY(ModuleType.PERSON, ModuleType.HOME),
@SerializedName("person_home") // When the person is declared at home
PERSON_HOME(ModuleType.PERSON, ModuleType.HOME),
@SerializedName("outdoor") // When the Outdoor Camera detects a human, a car or an animal
OUTDOOR(ModuleType.PRESENCE, ModuleType.DOORBELL),
@SerializedName("daily_summary") // When the Outdoor Camera video summary of the last 24 hours is available
DAILY_SUMMARY(ModuleType.PRESENCE),
@SerializedName("movement") // When the Indoor Camera detects motion
MOVEMENT(ModuleType.WELCOME),
@SerializedName("human") // When the Indoor Camera detects human motion
HUMAN(ModuleType.WELCOME),
@SerializedName("animal") // When the Indoor Camera detects animal motion
ANIMAL(ModuleType.WELCOME),
@SerializedName("new_module") // A new Module has been paired with the Indoor Camera
NEW_MODULE(ModuleType.WELCOME),
@SerializedName("module_connect") // Module is connected with the Indoor Camera
MODULE_CONNECT(ModuleType.WELCOME),
@SerializedName("module_disconnect") // Module lost its connection with the Indoor Camera
MODULE_DISCONNECT(ModuleType.WELCOME),
@SerializedName("module_low_battery") // Module's battery is low
MODULE_LOW_BATTERY(ModuleType.WELCOME),
@SerializedName("module_end_update") // Module's firmware update is over
MODULE_END_UPDATE(ModuleType.WELCOME),
@SerializedName("connection") // When the Camera connects to Netatmo servers
CONNECTION(ModuleType.WELCOME, ModuleType.PRESENCE),
@SerializedName("disconnection") // When the Camera loses connection with Netatmo servers
DISCONNECTION(ModuleType.WELCOME, ModuleType.PRESENCE),
@SerializedName("on") // When Camera Monitoring is resumed
ON(ModuleType.WELCOME, ModuleType.PRESENCE),
@SerializedName("off") // When Camera Monitoring is turned off
OFF(ModuleType.WELCOME, ModuleType.PRESENCE),
@SerializedName("boot") // When the Camera is booting
BOOT(ModuleType.WELCOME, ModuleType.PRESENCE),
@SerializedName("sd") // When Camera SD Card status changes
SD(ModuleType.WELCOME, ModuleType.PRESENCE),
@SerializedName("alim") // When Camera power supply status changes
ALIM(ModuleType.WELCOME, ModuleType.PRESENCE);
private final Set<ModuleType> appliesTo;
EventType(ModuleType... appliesTo) {
this.appliesTo = Set.of(appliesTo);
}
@Override
public String toString() {
return name().toLowerCase();
}
public boolean appliesOn(ModuleType searched) {
return appliesTo.contains(searched);
}
}

View File

@ -0,0 +1,225 @@
/**
* 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.api.data;
import static org.openhab.binding.netatmo.internal.NetatmoBindingConstants.BINDING_ID;
import static org.openhab.binding.netatmo.internal.api.data.NetatmoConstants.*;
import java.net.URI;
import java.util.EnumSet;
import java.util.LinkedList;
import java.util.List;
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.handler.capability.AirCareCapability;
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;
import org.openhab.binding.netatmo.internal.handler.capability.DeviceCapability;
import org.openhab.binding.netatmo.internal.handler.capability.EventCapability;
import org.openhab.binding.netatmo.internal.handler.capability.HomeCapability;
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.WeatherCapability;
import org.openhab.binding.netatmo.internal.handler.channelhelper.AirQualityChannelHelper;
import org.openhab.binding.netatmo.internal.handler.channelhelper.AirQualityExtChannelHelper;
import org.openhab.binding.netatmo.internal.handler.channelhelper.BatteryChannelHelper;
import org.openhab.binding.netatmo.internal.handler.channelhelper.BatteryExtChannelHelper;
import org.openhab.binding.netatmo.internal.handler.channelhelper.CameraChannelHelper;
import org.openhab.binding.netatmo.internal.handler.channelhelper.ChannelHelper;
import org.openhab.binding.netatmo.internal.handler.channelhelper.EventChannelHelper;
import org.openhab.binding.netatmo.internal.handler.channelhelper.EventPersonChannelHelper;
import org.openhab.binding.netatmo.internal.handler.channelhelper.HomeEnergyChannelHelper;
import org.openhab.binding.netatmo.internal.handler.channelhelper.HomeSecurityChannelHelper;
import org.openhab.binding.netatmo.internal.handler.channelhelper.HumidityChannelHelper;
import org.openhab.binding.netatmo.internal.handler.channelhelper.LocationChannelHelper;
import org.openhab.binding.netatmo.internal.handler.channelhelper.MeasuresChannelHelper;
import org.openhab.binding.netatmo.internal.handler.channelhelper.NoiseChannelHelper;
import org.openhab.binding.netatmo.internal.handler.channelhelper.PersonChannelHelper;
import org.openhab.binding.netatmo.internal.handler.channelhelper.PresenceChannelHelper;
import org.openhab.binding.netatmo.internal.handler.channelhelper.PressureChannelHelper;
import org.openhab.binding.netatmo.internal.handler.channelhelper.PressureExtChannelHelper;
import org.openhab.binding.netatmo.internal.handler.channelhelper.RainChannelHelper;
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.TemperatureChannelHelper;
import org.openhab.binding.netatmo.internal.handler.channelhelper.TemperatureExtChannelHelper;
import org.openhab.binding.netatmo.internal.handler.channelhelper.TemperatureOutChannelHelper;
import org.openhab.binding.netatmo.internal.handler.channelhelper.Therm1ChannelHelper;
import org.openhab.binding.netatmo.internal.handler.channelhelper.TimestampChannelHelper;
import org.openhab.binding.netatmo.internal.handler.channelhelper.TimestampExtChannelHelper;
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
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public enum ModuleType {
UNKNOWN(FeatureArea.NONE, null, null, List.of(), List.of()),
ACCOUNT(FeatureArea.NONE, null, 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")
PRESENCE(FeatureArea.SECURITY, "NOC", HOME,
List.of(EventCapability.class, PresenceCapability.class, ChannelHelperCapability.class),
List.of(CameraChannelHelper.class, 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(ChannelHelperCapability.class),
List.of(SignalChannelHelper.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),
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));
public static final EnumSet<ModuleType> AS_SET = EnumSet.allOf(ModuleType.class);
private final @Nullable ModuleType bridgeType;
public final List<String> groupTypes = new LinkedList<>();
public final List<String> extensions = new LinkedList<>();
public final List<Class<? extends ChannelHelper>> channelHelpers;
public final List<Class<? extends Capability>> capabilities;
public final ThingTypeUID thingTypeUID;
public final FeatureArea feature;
public final @Nullable String apiName;
ModuleType(FeatureArea feature, @Nullable String apiName, @Nullable ModuleType bridge,
List<Class<? extends Capability>> capabilities, List<Class<? extends ChannelHelper>> helpers) {
this.channelHelpers = helpers;
this.bridgeType = bridge;
this.feature = feature;
this.capabilities = capabilities;
this.apiName = apiName;
thingTypeUID = new ThingTypeUID(BINDING_ID, name().toLowerCase().replace("_", "-"));
try {
for (Class<? extends ChannelHelper> helperClass : helpers) {
ChannelHelper helper = helperClass.getConstructor().newInstance();
groupTypes.addAll(helper.getChannelGroupTypes());
extensions.addAll(helper.getExtensibleChannels());
}
} catch (RuntimeException | ReflectiveOperationException e) {
throw new IllegalArgumentException(e);
}
}
public boolean isLogical() {
return !channelHelpers.contains(SignalChannelHelper.class);
}
public boolean isABridge() {
for (ModuleType mt : ModuleType.values()) {
if (this.equals(mt.bridgeType)) {
return true;
}
}
return false;
}
public int[] getSignalLevels() {
if (!isLogical()) {
return (channelHelpers.contains(BatteryChannelHelper.class)
|| channelHelpers.contains(BatteryExtChannelHelper.class)) ? RADIO_SIGNAL_LEVELS
: WIFI_SIGNAL_LEVELS;
}
throw new IllegalArgumentException(
"This should not be called for module type : " + name() + ", please file a bug report.");
}
public ModuleType getBridge() {
ModuleType bridge = bridgeType;
return bridge != null ? bridge : ModuleType.UNKNOWN;
}
public URI getConfigDescription() {
return URI.create(BINDING_ID + ":"
+ (equals(ACCOUNT) ? "api_bridge"
: equals(HOME) ? "home"
: (isLogical() ? "virtual"
: ModuleType.UNKNOWN.equals(getBridge()) ? "configurable" : "device")));
}
public static ModuleType from(ThingTypeUID thingTypeUID) {
return ModuleType.AS_SET.stream().filter(mt -> mt.thingTypeUID.equals(thingTypeUID)).findFirst()
.orElseThrow(() -> new IllegalArgumentException());
}
public static ModuleType from(String apiName) {
return ModuleType.AS_SET.stream().filter(mt -> apiName.equals(mt.apiName)).findFirst()
.orElseThrow(() -> new IllegalArgumentException());
}
}

View File

@ -0,0 +1,401 @@
/**
* 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.api.data;
import static org.openhab.binding.netatmo.internal.NetatmoBindingConstants.*;
import static org.openhab.core.library.CoreItemFactory.*;
import static org.openhab.core.library.unit.MetricPrefix.*;
import java.net.URI;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.measure.Unit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.library.unit.SIUnits;
import org.openhab.core.library.unit.Units;
import org.openhab.core.types.StateDescriptionFragment;
import org.openhab.core.types.StateDescriptionFragmentBuilder;
import org.openhab.core.types.util.UnitUtils;
import com.google.gson.annotations.SerializedName;
/**
* This class holds various definitions and settings provided by the Netatmo
* API documentation
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class NetatmoConstants {
public static class Measure {
public final double minValue;
public final double maxValue;
public final int scale;
public final Unit<?> unit;
private Measure(double minValue, double maxValue, double precision, Unit<?> unit) {
this.minValue = minValue;
this.maxValue = maxValue;
this.unit = unit;
String[] splitter = Double.valueOf(precision).toString().split("\\.");
if (splitter.length > 1) {
int dec = Integer.parseInt(splitter[1]);
this.scale = dec > 0 ? Integer.toString(dec).length() : 0;
} else {
this.scale = 0;
}
}
}
public static class MeasureChannelDetails {
private static final StateDescriptionFragmentBuilder BUILDER = StateDescriptionFragmentBuilder.create();
public final URI configURI;
public final String itemType;
public final StateDescriptionFragment stateDescriptionFragment;
private MeasureChannelDetails(String measureType, String itemType, String pattern) {
this.configURI = URI.create(String.join(":", BINDING_ID, measureType, "config"));
this.itemType = itemType;
this.stateDescriptionFragment = BUILDER.withReadOnly(true).withPattern(pattern).build();
}
}
public enum MeasureClass {
INSIDE_TEMPERATURE(0, 50, 0.3, SIUnits.CELSIUS, "temp", "measure", true),
OUTSIDE_TEMPERATURE(-40, 65, 0.3, SIUnits.CELSIUS, "temp", "measure", true),
HEAT_INDEX(-40, 65, 1, SIUnits.CELSIUS, "", "", false),
PRESSURE(260, 1260, 1, HECTO(SIUnits.PASCAL), "pressure", "measure", true),
CO2(0, 5000, 50, Units.PARTS_PER_MILLION, "co2", "measure", true),
NOISE(35, 120, 1, Units.DECIBEL, "noise", "measure", true),
RAIN_QUANTITY(0, 150, 0.1, MILLI(SIUnits.METRE), "sum_rain", "sum_rain", false),
RAIN_INTENSITY(0, 150, 0.1, Units.MILLIMETRE_PER_HOUR, "", "", false),
WIND_SPEED(0, 160, 1.8, SIUnits.KILOMETRE_PER_HOUR, "", "", false),
WIND_ANGLE(0, 360, 5, Units.DEGREE_ANGLE, "", "", false),
HUMIDITY(0, 100, 3, Units.PERCENT, "hum", "measure", true);
public static final EnumSet<MeasureClass> AS_SET = EnumSet.allOf(MeasureClass.class);
public final Measure measureDefinition;
public final String apiDescriptor;
public final Map<String, MeasureChannelDetails> channels = new HashMap<>(2);
MeasureClass(double min, double max, double precision, Unit<?> unit, String apiDescriptor, String confFragment,
boolean canScale) {
this.measureDefinition = new Measure(min, max, precision, unit);
this.apiDescriptor = apiDescriptor;
if (!apiDescriptor.isBlank()) {
String dimension = UnitUtils.getDimensionName(unit);
channels.put(String.join("-", apiDescriptor, "measurement"),
new MeasureChannelDetails(confFragment, String.join(":", NUMBER, dimension),
String.format("%%.%df %s", measureDefinition.scale, UnitUtils.UNIT_PLACEHOLDER)));
if (canScale) {
channels.put(String.join("-", apiDescriptor, GROUP_TIMESTAMP),
new MeasureChannelDetails(GROUP_TIMESTAMP, DATETIME, "%1$tA, %1$td.%1$tm. %1$tH:%1$tM"));
}
}
}
}
// Netatmo API urls
public static final String URL_API = "https://api.netatmo.com/";
public static final String URL_APP = "https://app.netatmo.net/";
public static final String PATH_OAUTH = "oauth2/token";
public static final String PATH_API = "api";
public static final String PATH_COMMAND = "command";
public static final String SUB_PATH_PERSON_AWAY = "setpersonsaway";
public static final String SUB_PATH_PERSON_HOME = "setpersonshome";
public static final String SUB_PATH_HOMES_DATA = "homesdata";
public static final String SUB_PATH_ADDWEBHOOK = "addwebhook";
public static final String SUB_PATH_DROPWEBHOOK = "dropwebhook";
public static final String SUB_PATH_SETROOMTHERMPOINT = "setroomthermpoint";
public static final String SUB_PATH_SETTHERMMODE = "setthermmode";
public static final String SUB_PATH_SWITCHSCHEDULE = "switchschedule";
public static final String SUB_PATH_GETSTATION = "getstationsdata";
public static final String SUB_PATH_GETMEASURE = "getmeasure";
public static final String SUB_PATH_HOMESTATUS = "homestatus";
public static final String SUB_PATH_HOMECOACH = "gethomecoachsdata";
public static final String SUB_PATH_GETEVENTS = "getevents";
public static final String SUB_PATH_PING = "ping";
public static final String SUB_PATH_CHANGESTATUS = "changestatus";
public static final String SUB_PATH_FLOODLIGHTSET = "floodlight_set_config";
public static final String PARAM_DEVICEID = "device_id";
public static final String PARAM_MODULEID = "module_id";
public static final String PARAM_HOMEID = "home_id";
public static final String PARAM_ROOMID = "room_id";
public static final String PARAM_PERSONID = "person_id";
public static final String PARAM_SCHEDULEID = "schedule_id";
public static final String PARAM_OFFSET = "offset";
public static final String PARAM_GATEWAYTYPE = "gateway_types";
public static final String PARAM_MODE = "mode";
public static final String PARAM_URL = "url";
public static final String PARAM_FAVORITES = "get_favorites";
public static final String PARAM_STATUS = "status";
// Global variables
public static final int THERM_MAX_SETPOINT = 30;
// Token scopes
public static enum Scope {
@SerializedName("read_station")
READ_STATION,
@SerializedName("read_thermostat")
READ_THERMOSTAT,
@SerializedName("write_thermostat")
WRITE_THERMOSTAT,
@SerializedName("read_camera")
READ_CAMERA,
@SerializedName("write_camera")
WRITE_CAMERA,
@SerializedName("access_camera")
ACCESS_CAMERA,
@SerializedName("read_presence")
READ_PRESENCE,
@SerializedName("write_presence")
WRITE_PRESENCE,
@SerializedName("access_presence")
ACCESS_PRESENCE,
@SerializedName("read_smokedetector")
READ_SMOKEDETECTOR,
@SerializedName("read_homecoach")
READ_HOMECOACH,
@SerializedName("read_doorbell")
READ_DOORBELL,
@SerializedName("write_doorbell")
WRITE_DOORBELL,
@SerializedName("access_doorbell")
ACCESS_DOORBELL,
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
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)),
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 final Set<Scope> scopes;
FeatureArea(Scope... scopes) {
this.scopes = Set.of(scopes);
}
}
// Thermostat definitions
public static enum SetpointMode {
@SerializedName("program")
PROGRAM("program"),
@SerializedName("away")
AWAY("away"),
@SerializedName("hg")
FROST_GUARD("hg"),
@SerializedName("manual")
MANUAL("manual"),
@SerializedName("off")
OFF("off"),
@SerializedName("max")
MAX("max"),
@SerializedName("schedule")
SCHEDULE("schedule"),
HOME("home"),
UNKNOWN("");
public final String apiDescriptor;
SetpointMode(String descriptor) {
this.apiDescriptor = descriptor;
}
}
public static enum ThermostatZoneType {
@SerializedName("0")
DAY("0"),
@SerializedName("1")
NIGHT("1"),
@SerializedName("2")
AWAY("2"),
@SerializedName("3")
FROST_GUARD("3"),
@SerializedName("4")
CUSTOM("4"),
@SerializedName("5")
ECO("5"),
@SerializedName("8")
COMFORT("8"),
UNKNOWN("");
public final String zoneId;
private ThermostatZoneType(String id) {
zoneId = id;
}
}
public enum FloodLightMode {
@SerializedName("on")
ON,
@SerializedName("off")
OFF,
@SerializedName("auto")
AUTO,
UNKNOWN;
}
public enum EventCategory {
@SerializedName("human")
HUMAN,
@SerializedName("animal")
ANIMAL,
@SerializedName("vehicle")
VEHICLE,
UNKNOWN;
}
public enum TrendDescription {
@SerializedName("up")
UP,
@SerializedName("stable")
STABLE,
@SerializedName("down")
DOWN,
UNKNOWN;
}
public enum VideoStatus {
@SerializedName("recording")
RECORDING,
@SerializedName("available")
AVAILABLE,
@SerializedName("deleted")
DELETED,
UNKNOWN;
}
public enum SdCardStatus {
@SerializedName("1")
SD_CARD_MISSING,
@SerializedName("2")
SD_CARD_INSERTED,
@SerializedName("3")
SD_CARD_FORMATTED,
@SerializedName("4")
SD_CARD_WORKING,
@SerializedName("5")
SD_CARD_DEFECTIVE,
@SerializedName("6")
SD_CARD_INCOMPATIBLE_SPEED,
@SerializedName("7")
SD_CARD_INSUFFICIENT_SPACE,
UNKNOWN;
}
public enum AlimentationStatus {
@SerializedName("1")
ALIM_INCORRECT_POWER,
@SerializedName("2")
ALIM_CORRECT_POWER,
UNKNOWN;
}
public enum BatteryState {
@SerializedName("full")
FULL(100),
@SerializedName("high")
HIGH(80),
@SerializedName("medium")
MEDIUM(50),
@SerializedName("low")
LOW(15),
UNKNOWN(-1);
public final int level;
BatteryState(int i) {
this.level = i;
}
}
public enum ServiceError {
@SerializedName("99")
UNKNOWN,
@SerializedName("-2")
UNKNOWN_ERROR_IN_OAUTH,
@SerializedName("-1")
GRANT_IS_INVALID,
@SerializedName("1")
ACCESS_TOKEN_MISSING,
@SerializedName("2")
INVALID_TOKEN_MISSING,
@SerializedName("3")
ACCESS_TOKEN_EXPIRED,
@SerializedName("5")
APPLICATION_DEACTIVATED,
@SerializedName("7")
NOTHING_TO_MODIFY,
@SerializedName("9")
DEVICE_NOT_FOUND,
@SerializedName("10")
MISSING_ARGUMENTS,
@SerializedName("13")
OPERATION_FORBIDDEN,
@SerializedName("19")
IP_NOT_FOUND,
@SerializedName("21")
INVALID_ARGUMENT,
@SerializedName("22")
APPLICATION_NOT_FOUND,
@SerializedName("23")
USER_NOT_FOUND,
@SerializedName("25")
INVALID_DATE,
@SerializedName("26")
MAXIMUM_USAGE_REACHED,
@SerializedName("30")
INVALID_REFRESH_TOKEN,
@SerializedName("31")
METHOD_NOT_FOUND,
@SerializedName("35")
UNABLE_TO_EXECUTE,
@SerializedName("36")
PROHIBITED_STRING,
@SerializedName("37")
NO_MORE_SPACE_AVAILABLE_ON_THE_CAMERA,
@SerializedName("40")
JSON_GIVEN_HAS_AN_INVALID_ENCODING,
@SerializedName("41")
DEVICE_IS_UNREACHABLE;
}
}

View File

@ -0,0 +1,72 @@
/**
* 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.api.dto;
import java.util.List;
import org.openhab.binding.netatmo.internal.api.data.NetatmoConstants.Scope;
/**
* This is the Access Token Response, a simple value-object holding the result of an Access Token Request, as
* provided by Netatmo API.
*
* @author Gaël L'hopital - Initial contribution
*/
public final class AccessTokenResponse {
/**
* The access token issued by the authorization server. It is used
* by the client to gain access to a resource.
*
*/
private String accessToken;
/**
* Number of seconds that this OAuthToken is valid for since the time it was created.
*
*/
private long expiresIn;
/**
* Refresh token is a string representing the authorization granted to
* the client by the resource owner. Unlike access tokens, refresh tokens are
* intended for use only with authorization servers and are never sent
* to resource servers.
*
*/
private String refreshToken;
private List<Scope> scope;
public String getAccessToken() {
return accessToken;
}
public long getExpiresIn() {
return expiresIn;
}
public String getRefreshToken() {
return refreshToken;
}
public List<Scope> getScope() {
return scope;
}
@Override
public String toString() {
return "AccessTokenResponse [accessToken=" + accessToken + ", expiresIn=" + expiresIn + ", refreshToken="
+ refreshToken + ", scope=" + scope + "]";
}
}

View File

@ -0,0 +1,186 @@
/**
* 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.api.dto;
import java.time.ZonedDateTime;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.netatmo.internal.api.data.NetatmoConstants.TrendDescription;
import com.google.gson.annotations.SerializedName;
/**
* The {@link Dashboard} holds data returned by API call supporting the dashboard functionality.
*
* @author Gaël L'hopital - Initial contribution
*
*/
@NonNullByDefault
public class Dashboard {
private @Nullable ZonedDateTime timeUtc;
@SerializedName("BoilerOn")
private int boilerOn;
@SerializedName("BoilerOff")
private int boilerOff;
@SerializedName("Temperature")
private double temperature;
private TrendDescription pressureTrend = TrendDescription.UNKNOWN;
private TrendDescription tempTrend = TrendDescription.UNKNOWN;
private @Nullable ZonedDateTime dateMaxTemp;
private @Nullable ZonedDateTime dateMinTemp;
private double minTemp;
private double maxTemp;
@SerializedName("AbsolutePressure")
private double absolutePressure;
@SerializedName("CO2")
private double co2;
@SerializedName("Humidity")
private double humidity;
@SerializedName("Noise")
private double noise;
@SerializedName("Pressure")
private double pressure;
@SerializedName("Rain")
private double rain;
@SerializedName("sum_rain_1")
private double sumRain1;
@SerializedName("sum_rain_24")
private double sumRain24;
@SerializedName("WindAngle")
private int windAngle;
@SerializedName("GustAngle")
private int gustAngle;
@SerializedName("WindStrength")
private int windStrength;
private int maxWindStr;
private @Nullable ZonedDateTime dateMaxWindStr;
@SerializedName("GustStrength")
private int gustStrength;
private int healthIdx;
public @Nullable ZonedDateTime getTimeUtc() {
return timeUtc;
}
public int getBoilerOn() {
return boilerOn;
}
public int getBoilerOff() {
return boilerOff;
}
public double getTemperature() {
return temperature;
}
public TrendDescription getTempTrend() {
return tempTrend;
}
public @Nullable ZonedDateTime getDateMaxTemp() {
return dateMaxTemp;
}
public @Nullable ZonedDateTime getDateMinTemp() {
return dateMinTemp;
}
public double getMinTemp() {
return minTemp;
}
public double getMaxTemp() {
return maxTemp;
}
public double getAbsolutePressure() {
return absolutePressure;
}
public double getCo2() {
return co2;
}
public double getHumidity() {
return humidity;
}
public double getNoise() {
return noise;
}
public double getPressure() {
return pressure;
}
public TrendDescription getPressureTrend() {
return pressureTrend;
}
public double getRain() {
return rain;
}
public double getSumRain1() {
return sumRain1;
}
public double getSumRain24() {
return sumRain24;
}
public double getWindAngle() {
return windAngle;
}
public double getGustAngle() {
return gustAngle;
}
public double getWindStrength() {
return windStrength;
}
public double getMaxWindStr() {
return maxWindStr;
}
public @Nullable ZonedDateTime getDateMaxWindStr() {
return dateMaxWindStr;
}
public double getGustStrength() {
return gustStrength;
}
public int getHealthIdx() {
return healthIdx;
}
}

View File

@ -0,0 +1,50 @@
/**
* 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.api.dto;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.netatmo.internal.deserialization.NAObjectMap;
/**
* The {@link Device} holds common data for all Netatmo devices.
*
* @author Gaël L'hopital - Initial contribution
*
*/
@NonNullByDefault
public class Device extends NAThing {
private @Nullable NAObjectMap<Module> modules;
private long dateSetup;
private long lastUpgrade;
private @Nullable Place place;
public NAObjectMap<Module> getModules() {
NAObjectMap<Module> localModules = modules;
return localModules != null ? localModules : new NAObjectMap<>();
}
public long getDateSetup() {
return dateSetup;
}
public long getLastUpgrade() {
return lastUpgrade;
}
public Optional<Place> getPlace() {
return Optional.ofNullable(place);
}
}

View File

@ -0,0 +1,68 @@
/**
* 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.api.dto;
import java.time.ZonedDateTime;
import java.util.Optional;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.netatmo.internal.api.data.EventSubType;
import org.openhab.binding.netatmo.internal.api.data.EventType;
import com.google.gson.annotations.SerializedName;
/**
* The {@link Event} holds information transferred by the webhook.
*
* @author Gaël L'hopital - Initial contribution
*
*/
@NonNullByDefault
public abstract class Event extends NAObject {
protected EventType type = EventType.UNKNOWN;
@SerializedName(value = "camera_id", alternate = { "module_id" })
private String cameraId = "";
protected int subType = -1;
public abstract ZonedDateTime getTime();
public abstract @Nullable String getSnapshotUrl();
public abstract @Nullable String getPersonId();
public EventType getEventType() {
return type;
}
public String getCameraId() {
return cameraId;
}
@Override
public @Nullable String getName() {
String localMessage = super.getName();
return (localMessage != null ? localMessage.replace("<b>", "").replace("</b>", "") : "");
}
public Optional<EventSubType> getSubTypeDescription() {
return Stream.of(EventSubType.values()).filter(v -> v.types.contains(getEventType()) && v.subType == subType)
.findFirst();
}
public void setEventType(EventType type) {
this.type = type;
}
}

View File

@ -0,0 +1,51 @@
/**
* 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.api.dto;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.netatmo.internal.api.data.ModuleType;
/**
* The {@link Home} holds home information.
*
* @author Gaël L'hopital - Initial contribution
*
*/
@NonNullByDefault
public class Home extends Device implements Location {
private double[] coordinates = {};
private double altitude;
private List<HomeEvent> events = List.of();
@Override
public ModuleType getType() {
return ModuleType.HOME;
}
@Override
public double getAltitude() {
return altitude;
}
@Override
public double[] getCoordinates() {
return coordinates;
}
public List<HomeEvent> getEvents() {
return events;
}
}

View File

@ -0,0 +1,119 @@
/**
* 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.api.dto;
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.ApiResponse;
import org.openhab.binding.netatmo.internal.api.ListBodyResponse;
import org.openhab.binding.netatmo.internal.api.data.ModuleType;
import org.openhab.binding.netatmo.internal.api.data.NetatmoConstants.FeatureArea;
import org.openhab.binding.netatmo.internal.api.data.NetatmoConstants.SetpointMode;
import org.openhab.binding.netatmo.internal.deserialization.NAObjectMap;
/**
* The {@link HomeData} holds home information returned by homesdata endpoint.
*
* @author Gaël L'hopital - Initial contribution
*
*/
@NonNullByDefault
public class HomeData extends NAThing implements NAModule, LocationEx {
public class HomesDataResponse extends ApiResponse<ListBodyResponse<HomeData>> {
}
private double altitude;
private double[] coordinates = {};
private @Nullable String country;
private @Nullable String timezone;
private @Nullable String temperatureControlMode;
private SetpointMode thermMode = SetpointMode.UNKNOWN;
private int thermSetpointDefaultDuration;
private List<ThermProgram> schedules = List.of();
private NAObjectMap<HomeDataPerson> persons = new NAObjectMap<>();
private NAObjectMap<HomeDataRoom> rooms = new NAObjectMap<>();
private NAObjectMap<HomeDataModule> modules = new NAObjectMap<>();
@Override
public ModuleType getType() {
return ModuleType.HOME;
}
@Override
public double getAltitude() {
return altitude;
}
@Override
public double[] getCoordinates() {
return coordinates;
}
@Override
public Optional<String> getCountry() {
return Optional.ofNullable(country);
}
@Override
public Optional<String> getTimezone() {
return Optional.ofNullable(timezone);
}
public int getThermSetpointDefaultDuration() {
return thermSetpointDefaultDuration;
}
public SetpointMode getThermMode() {
return thermMode;
}
public NAObjectMap<HomeDataPerson> getPersons() {
return persons;
}
public List<HomeDataPerson> getKnownPersons() {
return persons.values().stream().filter(HomeDataPerson::isKnown).collect(Collectors.toList());
}
public Optional<String> getTemperatureControlMode() {
return Optional.ofNullable(temperatureControlMode);
}
public NAObjectMap<HomeDataRoom> getRooms() {
return rooms;
}
public NAObjectMap<HomeDataModule> getModules() {
return modules;
}
public Set<FeatureArea> getFeatures() {
return getModules().values().stream().map(m -> m.getType().feature).collect(Collectors.toSet());
}
public List<ThermProgram> getThermSchedules() {
return schedules;
}
public @Nullable ThermProgram getActiveProgram() {
return schedules.stream().filter(ThermProgram::isSelected).findFirst().orElse(null);
}
}

View File

@ -0,0 +1,46 @@
/**
* 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.api.dto;
import java.time.ZonedDateTime;
import java.util.List;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link HomeDataModule} holds module informations returned by getHomeData endpoint
*
* @author Gaël L'hopital - Initial contribution
*
*/
@NonNullByDefault
public class HomeDataModule extends NAThing implements NAModule {
private @Nullable ZonedDateTime setupDate;
private @Nullable String applianceType;
private List<String> moduleBridged = List.of();
public @Nullable String getApplianceType() {
return applianceType;
}
public Optional<ZonedDateTime> getSetupDate() {
return Optional.ofNullable(setupDate);
}
public List<String> getModuleBridged() {
return moduleBridged;
}
}

View File

@ -0,0 +1,44 @@
/**
* 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.api.dto;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.netatmo.internal.api.data.ModuleType;
/**
* The {@link HomeDataPerson} provides Person informations returned by getHomeData endpoint
*
* @author Gaël L'hopital - Initial contribution
*
*/
@NonNullByDefault
public class HomeDataPerson extends NAThing implements NAModule {
private @Nullable String url;
@Override
public ModuleType getType() {
return ModuleType.PERSON;
}
public boolean isKnown() {
return description != null;
}
public Optional<String> getUrl() {
return Optional.ofNullable(url);
}
}

View File

@ -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.api.dto;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.netatmo.internal.api.data.ModuleType;
/**
* The {@link HomeDataRoom} provides Room informations returned by getHomeData endpoint
*
* @author Gaël L'hopital - Initial contribution
*
*/
@NonNullByDefault
public class HomeDataRoom extends NAObject implements NAModule {
private List<String> moduleIds = List.of();
@Override
public ModuleType getType() {
// In json api answer type for NARoom is used with free strings like kitchen, living...
return ModuleType.ROOM;
}
public List<String> getModuleIds() {
return moduleIds;
}
}

View File

@ -0,0 +1,96 @@
/**
* 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.api.dto;
import java.time.ZonedDateTime;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.netatmo.internal.api.ApiResponse;
import org.openhab.binding.netatmo.internal.api.BodyResponse;
import org.openhab.binding.netatmo.internal.api.data.EventSubType;
import org.openhab.binding.netatmo.internal.api.data.EventType;
import org.openhab.binding.netatmo.internal.api.data.NetatmoConstants.EventCategory;
import org.openhab.binding.netatmo.internal.api.data.NetatmoConstants.VideoStatus;
/**
* The {@link HomeEvent} holds information transferred by the webhook about a home event.
*
* @author Gaël L'hopital - Initial contribution
*
*/
@NonNullByDefault
public class HomeEvent extends Event {
public class NAEventsDataResponse extends ApiResponse<BodyResponse<Home>> {
}
private ZonedDateTime time = ZonedDateTime.now();
private @Nullable String personId;
private EventCategory category = EventCategory.UNKNOWN;
private @Nullable Snapshot snapshot;
private @Nullable String videoId;
private VideoStatus videoStatus = VideoStatus.UNKNOWN;
private boolean isArrival;
@Override
public ZonedDateTime getTime() {
return time;
}
@Override
public @Nullable String getPersonId() {
return personId;
}
public @Nullable String getVideoId() {
return videoId;
}
public VideoStatus getVideoStatus() {
return videoStatus;
}
@Override
public Optional<EventSubType> getSubTypeDescription() {
// Blend extra information provided by this kind of event in subcategories...
if (type == EventType.PERSON) {
subType = isArrival ? EventSubType.PERSON_ARRIVAL.subType : EventSubType.PERSON_SEEN.subType;
} else if (type == EventType.PERSON_HOME) {
subType = EventSubType.PERSON_ARRIVAL.subType;
} else if (type == EventType.PERSON_AWAY) {
subType = EventSubType.PERSON_DEPARTURE.subType;
} else if (type == EventType.HUMAN) {
subType = EventSubType.MOVEMENT_HUMAN.subType;
} else if (type == EventType.ANIMAL) {
subType = EventSubType.MOVEMENT_ANIMAL.subType;
} else {
if (category == EventCategory.ANIMAL) {
subType = EventSubType.MOVEMENT_ANIMAL.subType;
} else if (category == EventCategory.HUMAN) {
subType = EventSubType.MOVEMENT_HUMAN.subType;
} else if (category == EventCategory.VEHICLE) {
subType = EventSubType.MOVEMENT_VEHICLE.subType;
}
}
// ... and let ancestor do his work
return super.getSubTypeDescription();
}
@Override
public @Nullable String getSnapshotUrl() {
Snapshot localSnap = snapshot;
return localSnap != null ? localSnap.getUrl() : null;
}
}

View File

@ -0,0 +1,110 @@
/**
* 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.api.dto;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.netatmo.internal.api.data.NetatmoConstants.AlimentationStatus;
import org.openhab.binding.netatmo.internal.api.data.NetatmoConstants.BatteryState;
import org.openhab.binding.netatmo.internal.api.data.NetatmoConstants.FloodLightMode;
import org.openhab.binding.netatmo.internal.api.data.NetatmoConstants.SdCardStatus;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.OpenClosedType;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
/**
* The {@link HomeStatusModule} holds module informations returned by getHomeData endpoint
*
* @author Gaël L'hopital - Initial contribution
*
*/
@NonNullByDefault
public class HomeStatusModule extends NAThing {
private @Nullable String firmwareName;
private @Nullable String wifiState;
private @Nullable String status;
private @Nullable OnOffType monitoring;
private FloodLightMode floodlight = FloodLightMode.UNKNOWN;
private SdCardStatus sdStatus = SdCardStatus.UNKNOWN;
private AlimentationStatus alimStatus = AlimentationStatus.UNKNOWN;
private @Nullable String sirenStatus;
private @Nullable String vpnUrl;
private boolean isLocal;
private BatteryState batteryState = BatteryState.UNKNOWN;
private int batteryLevel;
private @Nullable OpenClosedType boilerStatus;
private boolean boilerValveComfortBoost;
public State getBoilerStatus() {
OpenClosedType status = boilerStatus;
return status != null ? status : UnDefType.NULL;
}
public boolean getBoilerValveComfortBoost() {
return boilerValveComfortBoost;
}
public Optional<String> getFirmwareName() {
return Optional.ofNullable(firmwareName);
}
public Optional<String> getWifiState() {
return Optional.ofNullable(wifiState);
}
public Optional<String> getStatus() {
return Optional.ofNullable(status);
}
public State getMonitoring() {
OnOffType localStatus = monitoring;
return localStatus != null ? localStatus : UnDefType.NULL;
}
public FloodLightMode getFloodlight() {
return floodlight;
}
public SdCardStatus getSdStatus() {
return sdStatus;
}
public AlimentationStatus getAlimStatus() {
return alimStatus;
}
public Optional<String> getSirenStatus() {
return Optional.ofNullable(sirenStatus);
}
public @Nullable String getVpnUrl() {
return vpnUrl;
}
public boolean isLocal() {
return isLocal;
}
public BatteryState getBatteryState() {
return batteryState;
}
public int getBatteryLevel() {
return batteryLevel;
}
}

View File

@ -0,0 +1,37 @@
/**
* 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.api.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.netatmo.internal.api.data.ModuleType;
/**
* The {@link HomeStatusPerson} provides Person informations returned by getHomeData endpoint
*
* @author Gaël L'hopital - Initial contribution
*
*/
@NonNullByDefault
public class HomeStatusPerson extends NAThing {
private boolean outOfSight;
@Override
public ModuleType getType() {
return ModuleType.PERSON;
}
public boolean isOutOfSight() {
return outOfSight;
}
}

View File

@ -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.api.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.PointType;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
/**
* The {@link Location} is the common interface for dto holding a location
*
* @author Gaël L'hopital - Initial contribution
*
*/
@NonNullByDefault
public interface Location {
double[] getCoordinates();
double getAltitude();
default State getLocation() {
double[] coordinates = getCoordinates();
return coordinates.length != 2 ? UnDefType.UNDEF
: new PointType(new DecimalType(coordinates[1]), new DecimalType(coordinates[0]),
new DecimalType(getAltitude()));
}
}

View File

@ -0,0 +1,30 @@
/**
* 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.api.dto;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link LocationEx} is the common interface for dto holding a extra location data
*
* @author Gaël L'hopital - Initial contribution
*
*/
@NonNullByDefault
public interface LocationEx extends Location {
public Optional<String> getCountry();
public Optional<String> getTimezone();
}

View File

@ -0,0 +1,44 @@
/**
* 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.api.dto;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link MeasureBodyElem} holds a list of values returned by getMeasure endpoint.
*
* @author Gaël L'hopital - Initial contribution
*
*/
@NonNullByDefault
public class MeasureBodyElem<T> {
private List<List<T>> value = List.of();
public List<List<T>> getValue() {
return value;
}
public @Nullable T getSingleValue() {
if (!value.isEmpty()) {
List<T> first = value.get(0);
if (!first.isEmpty()) {
return first.get(0);
}
}
return null;
}
}

View File

@ -0,0 +1,37 @@
/**
* 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.api.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.netatmo.internal.api.data.NetatmoConstants.BatteryState;
/**
* The {@link Module} holds status information of a Netatmo module.
*
* @author Gaël L'hopital - Initial contribution
*
*/
@NonNullByDefault
public class Module extends NAThing {
private BatteryState batteryState = BatteryState.UNKNOWN;
private int batteryPercent = -1;
public int getBatteryPercent() {
return batteryPercent != -1 ? batteryPercent : batteryState.level;
}
public BatteryState getBatteryState() {
return batteryState;
}
}

View File

@ -0,0 +1,59 @@
/**
* 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.api.dto;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.netatmo.internal.api.ApiResponse;
import org.openhab.binding.netatmo.internal.deserialization.NAObjectMap;
/**
* The {@link NAHomeStatus} holds data for a given home.
*
* @author Gaël L'hopital - Initial contribution
*
*/
@NonNullByDefault
public class NAHomeStatus {
public class NAHomeStatusResponse extends ApiResponse<NAHomeStatus> {
}
public class HomeStatus extends NAThing {
private @Nullable NAObjectMap<HomeStatusModule> modules;
private @Nullable NAObjectMap<HomeStatusPerson> persons;
private @Nullable NAObjectMap<Room> rooms;
public NAObjectMap<HomeStatusModule> getModules() {
NAObjectMap<HomeStatusModule> localModules = modules;
return localModules != null ? localModules : new NAObjectMap<>();
}
public NAObjectMap<HomeStatusPerson> getPersons() {
NAObjectMap<HomeStatusPerson> localPersons = persons;
return localPersons != null ? localPersons : new NAObjectMap<>();
}
public NAObjectMap<Room> getRooms() {
NAObjectMap<Room> localRooms = rooms;
return localRooms != null ? localRooms : new NAObjectMap<>();
}
}
private @Nullable HomeStatus home;
public Optional<HomeStatus> getHomeStatus() {
return Optional.ofNullable(home);
}
}

View File

@ -0,0 +1,54 @@
/**
* 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.api.dto;
import java.time.Duration;
import java.time.Instant;
import java.time.ZonedDateTime;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.netatmo.internal.api.ApiResponse;
import org.openhab.binding.netatmo.internal.api.ListBodyResponse;
/**
* The {@link NAMain} defines a weather or nhc device.
*
* @author Gaël L'hopital - Initial contribution
*
*/
@NonNullByDefault
public class NAMain extends Device {
public class StationDataResponse extends ApiResponse<ListBodyResponse<NAMain>> {
}
private boolean readOnly;
/**
* true when the user was invited to (or has favorited) a station, false when the user owns it
*
* @return readOnly
**/
public boolean isReadOnly() {
return readOnly;
}
public boolean hasFreshData(int dataFreshnessLimit) {
// check by comparing data freshness
ZonedDateTime localLastSeen = lastSeen;
if (localLastSeen != null && !getType().isLogical()) {
return Duration.between(localLastSeen.toInstant(), Instant.now()).getSeconds() < dataFreshnessLimit;
}
return true;
}
}

View File

@ -0,0 +1,32 @@
/**
* 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.api.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.netatmo.internal.api.data.ModuleType;
/**
* The {@link NAModule} is the common interface for dto holding module informations
*
* @author Gaël L'hopital - Initial contribution
*
*/
@NonNullByDefault
public interface NAModule {
public String getId();
public @Nullable String getName();
public ModuleType getType();
}

View File

@ -0,0 +1,50 @@
/**
* 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.api.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
/**
* The {@link NAObject} class is the base class for all objects
* returned by the Netatmo API.
*
* @author Gaël L'hopital - Initial contribution
*
*/
@NonNullByDefault
public class NAObject {
@SerializedName(value = "id", alternate = { "program_id", "_id", "event_id" })
protected String id = "";
@SerializedName(value = "name", alternate = { "module_name", "station_name", "pseudo", "message", "key" })
protected @Nullable String description;
private boolean ignoredForThingUpdate;
public String getId() {
return id;
}
public @Nullable String getName() {
return description;
}
public boolean isIgnoredForThingUpdate() {
return ignoredForThingUpdate;
}
public void setIgnoredForThingUpdate(boolean ignoredForThingUpdate) {
this.ignoredForThingUpdate = ignoredForThingUpdate;
}
}

View File

@ -0,0 +1,92 @@
/**
* 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.api.dto;
import java.time.ZonedDateTime;
import java.util.Optional;
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.annotations.SerializedName;
/**
* The {@link NAThing} is the base class for devices and modules.
*
* @author Gaël L'hopital - Initial contribution
*
*/
@NonNullByDefault
public class NAThing extends NAObject implements NAModule {
@SerializedName(value = "rf_status", alternate = { "wifi_status", "rf_strength", "wifi_strength" })
private int radioStatus = -1;
@SerializedName(value = "last_seen", alternate = { "last_therm_seen", "last_status_store", "last_plug_seen",
"last_message", "last_activity" })
protected @Nullable ZonedDateTime lastSeen;
@SerializedName(value = "firmware", alternate = { "firmware_revision" })
private @Nullable String firmware;
private @Nullable Boolean reachable;
private @Nullable Dashboard dashboardData;
private @Nullable String roomId;
private @Nullable String bridge;
private ModuleType type = ModuleType.UNKNOWN;
@Override
public ModuleType getType() {
return type;
}
public boolean isReachable() {
// This is not implemented on all devices/modules, so if absent we consider it is reachable
Boolean localReachable = this.reachable;
return localReachable != null ? localReachable : true;
}
public void setReachable(boolean reachable) {
this.reachable = reachable;
}
public @Nullable Dashboard getDashboardData() {
return dashboardData;
}
public @Nullable String getFirmware() {
return firmware;
}
public int getRadioStatus() {
return radioStatus;
}
public Optional<ZonedDateTime> getLastSeen() {
return Optional.ofNullable(lastSeen);
}
/**
* @return true if the equipment has no parent, meaning its a device.
*/
public boolean isDevice() {
return bridge == null;
}
public @Nullable String getBridge() {
return bridge;
}
public @Nullable String getRoomId() {
return roomId;
}
}

View File

@ -0,0 +1,43 @@
/**
* 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.api.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.netatmo.internal.api.data.ModuleType;
/**
* The {@link Person} holds answers provided in webhook events
*
* @author Gaël L'hopital - Initial contribution
*
*/
@NonNullByDefault
public class Person extends NAThing {
private @Nullable String faceUrl;
private boolean isKnown;
@Override
public ModuleType getType() {
return ModuleType.PERSON;
}
public @Nullable String getFaceUrl() {
return faceUrl;
}
public boolean isKnown() {
return isKnown;
}
}

View File

@ -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.api.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.netatmo.internal.api.ApiResponse;
/**
* The {@link Ping} hold url data for a camera module
*
* @author Gaël L'hopital - Initial contribution
*
*/
@NonNullByDefault
public class Ping extends ApiResponse<String> {
private String localUrl = "";
private @Nullable String productName;
@Override
public String getStatus() {
return localUrl;
}
@Override
public @Nullable String getBody() {
return productName;
}
}

View File

@ -0,0 +1,58 @@
/**
* 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.api.dto;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link Place} reports location information of a Netatmo system.
*
* @author Gaël L'hopital - Initial contribution
*
*/
@NonNullByDefault
public class Place implements LocationEx {
private @Nullable String city;
private @Nullable String country;
private @Nullable String timezone;
private double altitude;
private double[] location = {};
public Optional<String> getCity() {
return Optional.ofNullable(city);
}
@Override
public Optional<String> getCountry() {
return Optional.ofNullable(country);
}
@Override
public Optional<String> getTimezone() {
return Optional.ofNullable(timezone);
}
@Override
public double getAltitude() {
return altitude;
}
@Override
public double[] getCoordinates() {
return location;
}
}

View File

@ -0,0 +1,86 @@
/**
* 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.api.dto;
import java.time.ZonedDateTime;
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.data.NetatmoConstants.SetpointMode;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.OpenClosedType;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
/**
* The {@link Room} holds temperature data for a given room.
*
* @author Bernhard Kreuz - Initial contribution
*
*/
@NonNullByDefault
public class Room extends NAObject implements NAModule {
private @Nullable String type;
private @Nullable OnOffType anticipating;
private boolean openWindow;
private @Nullable ZonedDateTime thermSetpointStartTime;
private @Nullable ZonedDateTime thermSetpointEndTime;
private SetpointMode thermSetpointMode = SetpointMode.UNKNOWN;
private int heatingPowerRequest;
private double thermMeasuredTemperature;
private double thermSetpointTemperature;
public State isAnticipating() {
OnOffType status = anticipating;
return status != null ? status : UnDefType.NULL;
}
public State hasOpenedWindows() {
return openWindow ? OpenClosedType.OPEN : OpenClosedType.CLOSED;
}
public int getHeatingPowerRequest() {
return heatingPowerRequest;
}
public double getMeasuredTemp() {
return thermMeasuredTemperature;
}
public SetpointMode getSetpointMode() {
return thermSetpointMode;
}
public double getSetpointTemp() {
return thermSetpointTemperature;
}
public @Nullable ZonedDateTime getSetpointBegin() {
return thermSetpointStartTime;
}
public @Nullable ZonedDateTime getSetpointEnd() {
return thermSetpointEndTime;
}
@Override
public ModuleType getType() {
// Note: In json api answer type for NARoom is used with words like kitchen, living...
return ModuleType.ROOM;
}
public @Nullable String getLocation() {
return type;
}
}

View File

@ -0,0 +1,32 @@
/**
* 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.api.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link Snapshot} holds data related to a snapshot.
*
* @author Gaël L'hopital - Initial contribution
*
*/
@NonNullByDefault
public class Snapshot {
private @Nullable String url;
public @Nullable String getUrl() {
return url;
}
}

View File

@ -0,0 +1,45 @@
/**
* 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.api.dto;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.netatmo.internal.deserialization.NAObjectMap;
/**
* The {@link ThermProgram} holds setpoint scheduling information.
*
* @author Gaël L'hopital - Initial contribution
*
*/
@NonNullByDefault
public class ThermProgram extends NAObject {
private NAObjectMap<Zone> zones = new NAObjectMap<>();
private List<TimeTableItem> timetable = List.of();
private boolean selected;
public List<TimeTableItem> getTimetable() {
return timetable;
}
public boolean isSelected() {
return selected;
}
public @Nullable Zone getZone(String id) {
return zones.get(id);
}
}

View File

@ -0,0 +1,36 @@
/**
* 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.api.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link TimeTableItem} holds the temp scheduling for a given zone.
*
* @author Gaël L'hopital - Initial contribution
*
*/
@NonNullByDefault
public class TimeTableItem extends NAObject {
private int mOffset;
private int zoneId;
public int getMinuteOffset() {
return mOffset;
}
public int getZoneId() {
return zoneId;
}
}

View File

@ -0,0 +1,66 @@
/**
* 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.api.dto;
import java.time.ZonedDateTime;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.netatmo.internal.api.data.EventType;
import org.openhab.binding.netatmo.internal.deserialization.NAObjectMap;
import org.openhab.binding.netatmo.internal.deserialization.NAPushType;
/**
* The {@link WebhookEvent} is responsible to hold
* data given back by the Netatmo API when calling the webhook
*
* @author Gaël L'hopital - Initial contribution
*
*/
@NonNullByDefault
public class WebhookEvent extends Event {
private @NonNullByDefault({}) NAPushType pushType;
private String homeId = "";
private @Nullable String snapshotUrl;
private NAObjectMap<Person> persons = new NAObjectMap<>();
// Webhook does not provide the event generation time, so we'll use the event reception time
private ZonedDateTime time = ZonedDateTime.now();
public String getHomeId() {
return homeId;
}
public NAObjectMap<Person> getPersons() {
return persons;
}
@Override
public EventType getEventType() {
return pushType.getEvent();
}
@Override
public ZonedDateTime getTime() {
return time;
}
@Override
public @Nullable String getPersonId() {
return persons.size() > 0 ? persons.keySet().iterator().next() : null;
}
@Override
public @Nullable String getSnapshotUrl() {
return snapshotUrl;
}
}

View File

@ -0,0 +1,37 @@
/**
* 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.api.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.netatmo.internal.api.data.NetatmoConstants.ThermostatZoneType;
/**
* The {@link Zone} holds temperature data for a given zone.
*
* @author Gaël L'hopital - Initial contribution
*
*/
@NonNullByDefault
public class Zone extends NAObject {
private ThermostatZoneType type = ThermostatZoneType.UNKNOWN;
private double temp;
public double getTemp() {
return temp;
}
public ThermostatZoneType getType() {
return type;
}
}

View File

@ -1,70 +0,0 @@
/**
* 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.camera;
import java.util.Objects;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* {@link CameraAddress} handles the data to address a camera (VPN and local address).
*
* @author Sven Strohschein - Initial contribution
*/
@NonNullByDefault
public class CameraAddress {
private final String vpnURL;
private final String localURL;
CameraAddress(final String vpnURL, final String localURL) {
this.vpnURL = vpnURL;
this.localURL = localURL;
}
public String getVpnURL() {
return vpnURL;
}
public String getLocalURL() {
return localURL;
}
/**
* Checks if the VPN URL was changed / isn't equal to the given VPN-URL.
*
* @param vpnURL old / known VPN URL
* @return true, when the VPN URL isn't equal given VPN URL, otherwise false
*/
public boolean isVpnURLChanged(String vpnURL) {
return !getVpnURL().equals(vpnURL);
}
@Override
public boolean equals(@Nullable Object object) {
if (this == object) {
return true;
}
if (object == null || getClass() != object.getClass()) {
return false;
}
CameraAddress that = (CameraAddress) object;
return vpnURL.equals(that.vpnURL) && localURL.equals(that.localURL);
}
@Override
public int hashCode() {
return Objects.hash(vpnURL, localURL);
}
}

View File

@ -1,246 +0,0 @@
/**
* 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.camera;
import static org.openhab.binding.netatmo.internal.ChannelTypeUtils.*;
import static org.openhab.binding.netatmo.internal.NetatmoBindingConstants.*;
import java.io.IOException;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.json.JSONException;
import org.json.JSONObject;
import org.openhab.binding.netatmo.internal.ChannelTypeUtils;
import org.openhab.binding.netatmo.internal.handler.NetatmoModuleHandler;
import org.openhab.core.i18n.TimeZoneProvider;
import org.openhab.core.io.net.http.HttpUtil;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.swagger.client.model.NAWelcomeCamera;
/**
* {@link CameraHandler} is the class used to handle Camera Data
*
* @author Sven Strohschein - Initial contribution (partly moved code from NAWelcomeCameraHandler to introduce
* inheritance, see NAWelcomeCameraHandler)
*
*/
@NonNullByDefault
public abstract class CameraHandler extends NetatmoModuleHandler<NAWelcomeCamera> {
private static final String PING_URL_PATH = "/command/ping";
private static final String STATUS_CHANGE_URL_PATH = "/command/changestatus";
private static final String LIVE_PICTURE = "/live/snapshot_720.jpg";
private final Logger logger = LoggerFactory.getLogger(CameraHandler.class);
private Optional<CameraAddress> cameraAddress;
protected CameraHandler(Thing thing, final TimeZoneProvider timeZoneProvider) {
super(thing, timeZoneProvider);
cameraAddress = Optional.empty();
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
String channelId = channelUID.getId();
switch (channelId) {
case CHANNEL_CAMERA_STATUS:
case CHANNEL_WELCOME_CAMERA_STATUS:
if (command == OnOffType.ON) {
switchVideoSurveillance(true);
} else if (command == OnOffType.OFF) {
switchVideoSurveillance(false);
}
break;
}
super.handleCommand(channelUID, command);
}
@Override
protected void updateProperties(NAWelcomeCamera moduleData) {
updateProperties(null, moduleData.getType());
}
@Override
protected State getNAThingProperty(String channelId) {
switch (channelId) {
case CHANNEL_CAMERA_STATUS:
return getStatusState();
case CHANNEL_CAMERA_SDSTATUS:
return getSdStatusState();
case CHANNEL_CAMERA_ALIMSTATUS:
return getAlimStatusState();
case CHANNEL_CAMERA_ISLOCAL:
return getIsLocalState();
case CHANNEL_CAMERA_LIVEPICTURE_URL:
return getLivePictureURLState();
case CHANNEL_CAMERA_LIVEPICTURE:
return getLivePictureState();
case CHANNEL_CAMERA_LIVESTREAM_URL:
return getLiveStreamState();
}
return super.getNAThingProperty(channelId);
}
protected State getStatusState() {
return getModule().map(m -> toOnOffType(m.getStatus())).orElse(UnDefType.UNDEF);
}
protected State getSdStatusState() {
return getModule().map(m -> toOnOffType(m.getSdStatus())).orElse(UnDefType.UNDEF);
}
protected State getAlimStatusState() {
return getModule().map(m -> toOnOffType(m.getAlimStatus())).orElse(UnDefType.UNDEF);
}
protected State getIsLocalState() {
return getModule().map(m -> toOnOffType(m.isIsLocal())).orElse(UnDefType.UNDEF);
}
protected State getLivePictureURLState() {
return getLivePictureURL().map(ChannelTypeUtils::toStringType).orElse(UnDefType.UNDEF);
}
protected State getLivePictureState() {
Optional<String> livePictureURL = getLivePictureURL();
return livePictureURL.isPresent() ? toRawType(livePictureURL.get()) : UnDefType.UNDEF;
}
protected State getLiveStreamState() {
return getLiveStreamURL().map(ChannelTypeUtils::toStringType).orElse(UnDefType.UNDEF);
}
/**
* Get the url for the live snapshot
*
* @return Url of the live snapshot
*/
private Optional<String> getLivePictureURL() {
return getVpnUrl().map(u -> u += LIVE_PICTURE);
}
/**
* Get the url for the live stream depending wether local or not
*
* @return Url of the live stream
*/
private Optional<String> getLiveStreamURL() {
Optional<String> result = getVpnUrl();
if (!result.isPresent()) {
return Optional.empty();
}
StringBuilder resultStringBuilder = new StringBuilder(result.get());
resultStringBuilder.append("/live/index");
if (isLocal()) {
resultStringBuilder.append("_local");
}
resultStringBuilder.append(".m3u8");
return Optional.of(resultStringBuilder.toString());
}
private Optional<String> getVpnUrl() {
return getModule().map(NAWelcomeCamera::getVpnUrl);
}
public Optional<String> getStreamURL(String videoId) {
Optional<String> result = getVpnUrl();
if (!result.isPresent()) {
return Optional.empty();
}
StringBuilder resultStringBuilder = new StringBuilder(result.get());
resultStringBuilder.append("/vod/");
resultStringBuilder.append(videoId);
resultStringBuilder.append("/index");
if (isLocal()) {
resultStringBuilder.append("_local");
}
resultStringBuilder.append(".m3u8");
return Optional.of(resultStringBuilder.toString());
}
private boolean isLocal() {
return getModule().map(NAWelcomeCamera::isIsLocal).orElse(false);
}
private void switchVideoSurveillance(boolean isOn) {
Optional<String> localCameraURL = getLocalCameraURL();
if (localCameraURL.isPresent()) {
String url = localCameraURL.get() + STATUS_CHANGE_URL_PATH + "?status=";
if (isOn) {
url += "on";
} else {
url += "off";
}
executeGETRequest(url);
invalidateParentCacheAndRefresh();
}
}
protected Optional<String> getLocalCameraURL() {
Optional<String> vpnURLOptional = getVpnUrl();
Optional<CameraAddress> address = cameraAddress;
if (vpnURLOptional.isPresent()) {
final String vpnURL = vpnURLOptional.get();
// The local address is (re-)requested when it wasn't already determined or when the vpn address was
// changed.
if (!address.isPresent() || address.get().isVpnURLChanged(vpnURL)) {
Optional<JSONObject> json = executeGETRequestJSON(vpnURL + PING_URL_PATH);
address = json.map(j -> j.optString("local_url", null))
.map(localURL -> new CameraAddress(vpnURL, localURL));
cameraAddress = address;
}
}
return address.map(CameraAddress::getLocalURL);
}
private Optional<JSONObject> executeGETRequestJSON(String url) {
try {
return executeGETRequest(url).map(JSONObject::new);
} catch (JSONException e) {
logger.warn("Error on parsing the content as JSON!", e);
}
return Optional.empty();
}
protected Optional<String> executeGETRequest(String url) {
try {
String content = HttpUtil.executeUrl("GET", url, 5000);
if (content != null && !content.isEmpty()) {
return Optional.of(content);
}
} catch (IOException e) {
logger.warn("Error on accessing local camera url!", e);
}
return Optional.empty();
}
@Override
protected boolean isReachable() {
Optional<NAWelcomeCamera> module = getModule();
return module.isPresent() ? !"disconnected".equalsIgnoreCase(module.get().getStatus()) : false;
}
}

View File

@ -1,80 +0,0 @@
/**
* 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.channelhelper;
import static org.openhab.binding.netatmo.internal.NetatmoBindingConstants.*;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.netatmo.internal.ChannelTypeUtils;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link BatteryHelper} handle specific behavior
* of modules using batteries
*
* @author Gaël L'hopital - Initial contribution
*
*/
@NonNullByDefault
public class BatteryHelper {
private final Logger logger = LoggerFactory.getLogger(BatteryHelper.class);
private int batteryLow;
private @Nullable Object module;
public BatteryHelper(String batteryLevels) {
List<String> thresholds = Arrays.asList(batteryLevels.split(","));
batteryLow = Integer.parseInt(thresholds.get(1));
}
public void setModule(Object module) {
this.module = module;
}
public Optional<State> getNAThingProperty(String channelId) {
Object module = this.module;
if (module != null) {
try {
if (CHANNEL_BATTERY_LEVEL.equalsIgnoreCase(channelId)
|| CHANNEL_LOW_BATTERY.equalsIgnoreCase(channelId)) {
switch (channelId) {
case CHANNEL_BATTERY_LEVEL:
Method getBatteryPercent = module.getClass().getMethod("getBatteryPercent");
Integer batteryPercent = (Integer) getBatteryPercent.invoke(module);
return Optional.of(ChannelTypeUtils.toDecimalType(batteryPercent));
case CHANNEL_LOW_BATTERY:
Method getBatteryVp = module.getClass().getMethod("getBatteryVp");
Integer batteryVp = (Integer) getBatteryVp.invoke(module);
return Optional.of(batteryVp < batteryLow ? OnOffType.ON : OnOffType.OFF);
}
}
} catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException
| InvocationTargetException e) {
logger.warn("The module has no method to access {} property : {}", channelId, e.getMessage());
return Optional.of(UnDefType.NULL);
}
}
return Optional.empty();
}
}

View File

@ -1,85 +0,0 @@
/**
* 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.channelhelper;
import static org.openhab.binding.netatmo.internal.NetatmoBindingConstants.*;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link RadioHelper} handle specific behavior
* of WIFI or RF devices and modules
*
* @author Gaël L'hopital - Initial contribution
*
*/
@NonNullByDefault
public class RadioHelper {
private final Logger logger = LoggerFactory.getLogger(RadioHelper.class);
private final List<Integer> signalThresholds;
private @Nullable Object module;
public RadioHelper(String signalLevels) {
signalThresholds = Stream.of(signalLevels.split(",")).map(Integer::parseInt).collect(Collectors.toList());
}
private int getSignalStrength(int signalLevel) {
int level;
for (level = 0; level < signalThresholds.size(); level++) {
if (signalLevel > signalThresholds.get(level)) {
break;
}
}
return level;
}
public void setModule(Object module) {
this.module = module;
}
public Optional<State> getNAThingProperty(String channelId) {
Object module = this.module;
if (module != null) {
try {
switch (channelId) {
case CHANNEL_RF_STATUS:
Method getRfStatus = module.getClass().getMethod("getRfStatus");
Integer rfStatus = (Integer) getRfStatus.invoke(module);
return Optional.of(new DecimalType(getSignalStrength(rfStatus)));
case CHANNEL_WIFI_STATUS:
Method getWifiStatus = module.getClass().getMethod("getWifiStatus");
Integer wifiStatus = (Integer) getWifiStatus.invoke(module);
return Optional.of(new DecimalType(getSignalStrength(wifiStatus)));
}
} catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException
| InvocationTargetException e) {
logger.warn("The module has no method to access {} property : {}", channelId, e.getMessage());
return Optional.of(UnDefType.NULL);
}
}
return Optional.empty();
}
}

View File

@ -0,0 +1,62 @@
/**
* 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.config;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.netatmo.internal.api.NetatmoException;
/**
* The {@link ApiHandlerConfiguration} is responsible for holding configuration
* information needed to access Netatmo API and general binding behavior setup
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class ApiHandlerConfiguration {
public class Credentials {
public final String clientId, clientSecret, username, password;
private Credentials(@Nullable String clientId, @Nullable String clientSecret, @Nullable String username,
@Nullable String password) throws NetatmoException {
this.clientSecret = checkMandatory(clientSecret, "@text/conf-error-no-client-secret");
this.username = checkMandatory(username, "@text/conf-error-no-username");
this.password = checkMandatory(password, "@text/conf-error-no-password");
this.clientId = checkMandatory(clientId, "@text/conf-error-no-client-id");
}
private String checkMandatory(@Nullable String value, String error) throws NetatmoException {
if (value == null || value.isBlank()) {
throw new NetatmoException(error);
}
return value;
}
@Override
public String toString() {
return "Credentials [clientId=" + clientId + ", username=" + username
+ ", password=******, clientSecret=******]";
}
}
private @Nullable String clientId;
private @Nullable String clientSecret;
private @Nullable String username;
private @Nullable String password;
public @Nullable String webHookUrl;
public int reconnectInterval = 300;
public Credentials getCredentials() throws NetatmoException {
return new Credentials(clientId, clientSecret, username, password);
}
}

View File

@ -0,0 +1,34 @@
/**
* 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.config;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.netatmo.internal.api.data.NetatmoConstants.FeatureArea;
/**
* The {@link BindingConfiguration} is responsible for holding configuration of the binding itself.
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class BindingConfiguration {
public Set<FeatureArea> features = Set.of();
public boolean readFriends = false;
public void update(BindingConfiguration newConfig) {
this.features = newConfig.features;
this.readFriends = newConfig.readFriends;
}
}

View File

@ -0,0 +1,27 @@
/**
* 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.config;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link MeasureConfiguration} is responsible for holding
* configuration information for measure channels
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class MeasureConfiguration {
public String period = "";
public String limit = "";
}

View File

@ -0,0 +1,27 @@
/**
* 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.config;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link NAThingConfiguration} is responsible for holding
* configuration information for any Netatmo thing module or device
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class NAThingConfiguration {
public String id = "";
public int refreshInterval = -1;
}

View File

@ -1,37 +0,0 @@
/**
* 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.config;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link NetatmoBridgeConfiguration} is responsible for holding
* configuration informations needed to access Netatmo API
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class NetatmoBridgeConfiguration {
public @Nullable String clientId;
public @Nullable String clientSecret;
public @Nullable String username;
public @Nullable String password;
public boolean readStation = true;
public boolean readThermostat = false;
public boolean readHealthyHomeCoach = false;
public boolean readWelcome = false;
public boolean readPresence = false;
public @Nullable String webHookUrl;
public int reconnectInterval = 5400;
}

View File

@ -0,0 +1,80 @@
/**
* 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.time.Instant;
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.core.i18n.TimeZoneProvider;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.OpenClosedType;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonSyntaxException;
/**
* The {@link NADeserializer} is responsible to instantiate suitable Gson (de)serializer
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
@Component(service = NADeserializer.class)
public class NADeserializer {
private final Gson gson;
@Activate
public NADeserializer(@Reference TimeZoneProvider timeZoneProvider) {
gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
.registerTypeAdapterFactory(new StrictEnumTypeAdapterFactory())
.registerTypeAdapter(NAObjectMap.class, new NAObjectMapDeserializer())
.registerTypeAdapter(NAPushType.class, new NAPushTypeDeserializer())
.registerTypeAdapter(ZonedDateTime.class,
(JsonDeserializer<ZonedDateTime>) (json, type, jsonDeserializationContext) -> {
long netatmoTS = json.getAsJsonPrimitive().getAsLong();
Instant i = Instant.ofEpochSecond(netatmoTS);
return ZonedDateTime.ofInstant(i, timeZoneProvider.getTimeZone());
})
.registerTypeAdapter(OnOffType.class,
(JsonDeserializer<OnOffType>) (json, type, jsonDeserializationContext) -> OnOffType
.from(json.getAsJsonPrimitive().getAsString()))
.registerTypeAdapter(OpenClosedType.class,
(JsonDeserializer<OpenClosedType>) (json, type, jsonDeserializationContext) -> {
String value = json.getAsJsonPrimitive().getAsString().toUpperCase();
return "TRUE".equals(value) || "1".equals(value) ? OpenClosedType.CLOSED
: OpenClosedType.OPEN;
})
.create();
}
public <T> T deserialize(Class<T> clazz, String json) throws NetatmoException {
try {
@Nullable
T result = gson.fromJson(json, clazz);
if (result != null) {
return result;
}
throw new NetatmoException("Deserialization of '%s' resulted in null value", json);
} catch (JsonSyntaxException e) {
throw new NetatmoException(e, "Unexpected error deserializing '%s'", json);
}
}
}

View File

@ -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.util.HashMap;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.netatmo.internal.api.dto.NAObject;
/**
* The {@link NAObjectMap} defines a hashmap of NAObjects identified by their id.
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class NAObjectMap<T extends NAObject> extends HashMap<String, T> {
private static final long serialVersionUID = 7635233672795516649L;
@Nullable
public T put(T thing) {
return super.put(thing.getId(), thing);
}
public Optional<T> getOpt(String key) {
return Optional.ofNullable(super.get(key));
}
}

View File

@ -0,0 +1,51 @@
/**
* 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.ParameterizedType;
import java.lang.reflect.Type;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.netatmo.internal.api.dto.NAObject;
import com.google.gson.JsonArray;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
/**
* The {@link NAObjectMapDeserializer} is a specialized deserializer aimed to transform
* a list of `NAObjects` into a map identified by the object's id.
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
class NAObjectMapDeserializer implements JsonDeserializer<NAObjectMap<?>> {
@Override
public @Nullable NAObjectMap<?> deserialize(JsonElement json, Type clazz, JsonDeserializationContext context)
throws JsonParseException {
ParameterizedType parameterized = (ParameterizedType) clazz;
Type[] typeArguments = parameterized.getActualTypeArguments();
if (typeArguments.length > 0 && json instanceof JsonArray) {
Type objectType = typeArguments[0];
NAObjectMap<NAObject> result = new NAObjectMap<>();
((JsonArray) json).forEach(item -> {
result.put(context.deserialize(item, objectType));
});
return result;
}
return null;
}
}

View File

@ -0,0 +1,41 @@
/**
* 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 org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.netatmo.internal.api.data.EventType;
import org.openhab.binding.netatmo.internal.api.data.ModuleType;
/**
* This class holds data of push_type field
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class NAPushType {
private final ModuleType moduleType;
private final EventType event;
NAPushType(ModuleType moduleType, EventType event) {
this.moduleType = moduleType;
this.event = event;
}
public ModuleType getModuleType() {
return moduleType;
}
public EventType getEvent() {
return event;
}
}

View File

@ -0,0 +1,51 @@
/**
* 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.EventType;
import org.openhab.binding.netatmo.internal.api.data.ModuleType;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
/**
* Specialized deserializer for push_type field
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
class NAPushTypeDeserializer implements JsonDeserializer<NAPushType> {
@Override
public @Nullable NAPushType deserialize(JsonElement json, Type clazz, JsonDeserializationContext context)
throws JsonParseException {
String string = json.getAsString();
String[] elements = string.split("-");
if (elements.length > 1) {
try {
ModuleType moduleType = ModuleType.from(elements[0]);
EventType eventType = EventType.valueOf(elements[1].toUpperCase());
return new NAPushType(moduleType, eventType);
} catch (IllegalArgumentException e) {
}
}
throw new JsonParseException("Error deserializing : " + string);
}
}

View File

@ -0,0 +1,67 @@
/**
* 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.io.IOException;
import java.io.StringReader;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.Gson;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
/**
* Enforces a fallback to UNKNOWN when deserializing enum types, marked as @NonNull whereas they were valued
* to null if the appropriate value is absent. It will give more resilience to the binding when Netatmo API evolves.
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
class StrictEnumTypeAdapterFactory implements TypeAdapterFactory {
private static final StringReader UNKNOWN = new StringReader("\"UNKNOWN\"");
@Override
public @Nullable <T> TypeAdapter<T> create(@NonNullByDefault({}) Gson gson,
@NonNullByDefault({}) TypeToken<T> type) {
@SuppressWarnings("unchecked")
Class<T> rawType = (Class<T>) type.getRawType();
return rawType.isEnum() ? newStrictEnumAdapter(gson.getDelegateAdapter(this, type)) : null;
}
private <T> TypeAdapter<T> newStrictEnumAdapter(@NonNullByDefault({}) TypeAdapter<T> delegateAdapter) {
return new TypeAdapter<T>() {
@Override
public void write(JsonWriter out, @Nullable T value) throws IOException {
delegateAdapter.write(out, value);
}
@Override
public @Nullable T read(JsonReader in) throws IOException {
JsonReader delegateReader = new JsonReader(new StringReader('"' + in.nextString() + '"'));
@Nullable
T value = delegateAdapter.read(delegateReader);
delegateReader.close();
if (value == null) {
UNKNOWN.reset();
value = delegateAdapter.read(new JsonReader(UNKNOWN));
}
return value;
}
};
}
}

View File

@ -0,0 +1,156 @@
/**
* 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.discovery;
import static org.openhab.binding.netatmo.internal.NetatmoBindingConstants.EQUIPMENT_ID;
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.AircareApi;
import org.openhab.binding.netatmo.internal.api.HomeApi;
import org.openhab.binding.netatmo.internal.api.ListBodyResponse;
import org.openhab.binding.netatmo.internal.api.NetatmoException;
import org.openhab.binding.netatmo.internal.api.WeatherApi;
import org.openhab.binding.netatmo.internal.api.data.ModuleType;
import org.openhab.binding.netatmo.internal.api.data.NetatmoConstants.FeatureArea;
import org.openhab.binding.netatmo.internal.api.dto.NAMain;
import org.openhab.binding.netatmo.internal.api.dto.NAModule;
import org.openhab.binding.netatmo.internal.config.BindingConfiguration;
import org.openhab.binding.netatmo.internal.handler.ApiBridgeHandler;
import org.openhab.core.config.discovery.AbstractDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.config.discovery.DiscoveryService;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link NetatmoDiscoveryService} searches for available Netatmo things
*
* @author Gaël L'hopital - Initial contribution
*
*/
@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 final Logger logger = LoggerFactory.getLogger(NetatmoDiscoveryService.class);
private @Nullable ApiBridgeHandler handler;
private @Nullable BindingConfiguration config;
public NetatmoDiscoveryService() {
super(ModuleType.AS_SET.stream().filter(mt -> !SKIPPED_TYPES.contains(mt)).map(mt -> mt.thingTypeUID)
.collect(Collectors.toSet()), DISCOVER_TIMEOUT_SECONDS);
}
@Override
public void startScan() {
BindingConfiguration localConf = config;
ApiBridgeHandler localHandler = handler;
if (localHandler != null && localConf != null) {
ThingUID apiBridgeUID = 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));
}
}
if (localConf.readFriends) {
WeatherApi weatherApi = localHandler.getRestManager(WeatherApi.class);
if (weatherApi != null) { // Search favorite stations
ListBodyResponse<NAMain> body = weatherApi.getStationsData(null, true).getBody();
if (body != null) {
body.getElements().stream().filter(NAMain::isReadOnly).forEach(station -> {
ThingUID bridgeUID = createThing(station, apiBridgeUID);
station.getModules().values().stream()
.forEach(module -> createThing(module, bridgeUID));
});
}
}
}
HomeApi homeApi = localHandler.getRestManager(HomeApi.class);
if (homeApi != null) { // Search all the rest
homeApi.getHomesData(null, null).stream().filter(h -> !h.getFeatures().isEmpty()).forEach(home -> {
ThingUID homeUID = createThing(home, apiBridgeUID);
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);
});
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));
});
});
}
} catch (NetatmoException e) {
logger.warn("Error during discovery process : {}", e.getMessage());
}
}
}
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 createThing(NAModule module, @Nullable ThingUID bridgeUID) {
ThingUID moduleUID = findThingUID(module.getType(), module.getId(), bridgeUID);
DiscoveryResultBuilder resultBuilder = DiscoveryResultBuilder.create(moduleUID)
.withProperty(EQUIPMENT_ID, module.getId()).withRepresentationProperty(EQUIPMENT_ID)
.withLabel(module.getName() != null ? module.getName() : module.getId());
if (bridgeUID != null) {
resultBuilder.withBridge(bridgeUID);
}
thingDiscovered(resultBuilder.build());
return moduleUID;
}
@Override
public void setThingHandler(ThingHandler handler) {
if (handler instanceof ApiBridgeHandler) {
this.handler = (ApiBridgeHandler) handler;
this.config = ((ApiBridgeHandler) handler).getConfiguration();
}
}
@Override
public @Nullable ThingHandler getThingHandler() {
return handler;
}
@Override
public void deactivate() {
super.deactivate();
}
}

View File

@ -1,253 +0,0 @@
/**
* 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.discovery;
import static org.openhab.binding.netatmo.internal.APIUtils.*;
import static org.openhab.binding.netatmo.internal.NetatmoBindingConstants.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.netatmo.internal.handler.NetatmoBridgeHandler;
import org.openhab.binding.netatmo.internal.handler.NetatmoDataListener;
import org.openhab.core.config.discovery.AbstractDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.i18n.LocaleProvider;
import org.openhab.core.i18n.TranslationProvider;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.osgi.framework.Bundle;
import org.osgi.framework.FrameworkUtil;
import io.swagger.client.model.NAHealthyHomeCoach;
import io.swagger.client.model.NAMain;
import io.swagger.client.model.NAPlug;
import io.swagger.client.model.NAStationModule;
import io.swagger.client.model.NAWelcomeCamera;
import io.swagger.client.model.NAWelcomeHome;
/**
* The {@link NetatmoModuleDiscoveryService} searches for available Netatmo
* devices and modules connected to the API console
*
* @author Gaël L'hopital - Initial contribution
* @author Ing. Peter Weiss - Welcome camera implementation
*
*/
@NonNullByDefault
public class NetatmoModuleDiscoveryService extends AbstractDiscoveryService implements NetatmoDataListener {
private static final int SEARCH_TIME = 5;
private final NetatmoBridgeHandler netatmoBridgeHandler;
public NetatmoModuleDiscoveryService(NetatmoBridgeHandler netatmoBridgeHandler, LocaleProvider localeProvider,
TranslationProvider translationProvider) {
super(SUPPORTED_DEVICE_THING_TYPES_UIDS, SEARCH_TIME);
this.netatmoBridgeHandler = netatmoBridgeHandler;
this.localeProvider = localeProvider;
this.i18nProvider = translationProvider;
}
@Override
public void activate(@Nullable Map<String, Object> configProperties) {
super.activate(configProperties);
netatmoBridgeHandler.registerDataListener(this);
}
@Override
public void deactivate() {
netatmoBridgeHandler.unregisterDataListener(this);
super.deactivate();
}
@Override
public void startScan() {
if (netatmoBridgeHandler.configuration.readStation) {
netatmoBridgeHandler.getStationsDataBody(null).ifPresent(dataBody -> {
nonNullList(dataBody.getDevices()).forEach(station -> {
discoverWeatherStation(station);
});
});
}
if (netatmoBridgeHandler.configuration.readHealthyHomeCoach) {
netatmoBridgeHandler.getHomecoachDataBody(null).ifPresent(dataBody -> {
nonNullList(dataBody.getDevices()).forEach(homecoach -> {
discoverHomeCoach(homecoach);
});
});
}
if (netatmoBridgeHandler.configuration.readThermostat) {
netatmoBridgeHandler.getThermostatsDataBody(null).ifPresent(dataBody -> {
nonNullList(dataBody.getDevices()).forEach(plug -> {
discoverThermostat(plug);
});
});
}
if (netatmoBridgeHandler.configuration.readWelcome || netatmoBridgeHandler.configuration.readPresence) {
netatmoBridgeHandler.getWelcomeDataBody(null).ifPresent(dataBody -> {
nonNullList(dataBody.getHomes()).forEach(home -> {
discoverWelcomeHome(home);
});
});
}
}
@Override
protected synchronized void stopScan() {
super.stopScan();
removeOlderResults(getTimestampOfLastScan(), netatmoBridgeHandler.getThing().getUID());
}
@Override
public void onDataRefreshed(Object data) {
if (!isBackgroundDiscoveryEnabled()) {
return;
}
if (data instanceof NAMain) {
discoverWeatherStation((NAMain) data);
} else if (data instanceof NAPlug) {
discoverThermostat((NAPlug) data);
} else if (data instanceof NAHealthyHomeCoach) {
discoverHomeCoach((NAHealthyHomeCoach) data);
} else if (data instanceof NAWelcomeHome) {
discoverWelcomeHome((NAWelcomeHome) data);
}
}
private void discoverThermostat(NAPlug plug) {
onDeviceAddedInternal(plug.getId(), null, plug.getType(), plug.getStationName(), plug.getFirmware());
nonNullList(plug.getModules()).forEach(thermostat -> {
onDeviceAddedInternal(thermostat.getId(), plug.getId(), thermostat.getType(), thermostat.getModuleName(),
thermostat.getFirmware());
});
}
private void discoverHomeCoach(NAHealthyHomeCoach homecoach) {
onDeviceAddedInternal(homecoach.getId(), null, homecoach.getType(), homecoach.getName(),
homecoach.getFirmware());
}
private void discoverWeatherStation(NAMain station) {
final boolean isFavorite = station.isFavorite() != null && station.isFavorite();
final String weatherStationName = createWeatherStationName(station, isFavorite);
onDeviceAddedInternal(station.getId(), null, station.getType(), weatherStationName, station.getFirmware());
nonNullList(station.getModules()).forEach(module -> {
onDeviceAddedInternal(module.getId(), station.getId(), module.getType(),
createWeatherModuleName(station, module, isFavorite), module.getFirmware());
});
}
private void discoverWelcomeHome(NAWelcomeHome home) {
// I observed that Thermostat homes are also reported here by Netatmo API
// So I ignore homes that have an empty list of cameras
List<NAWelcomeCamera> cameras = nonNullList(home.getCameras());
if (!cameras.isEmpty()) {
onDeviceAddedInternal(home.getId(), null, WELCOME_HOME_THING_TYPE.getId(), home.getName(), null);
// Discover Cameras
cameras.forEach(camera -> {
onDeviceAddedInternal(camera.getId(), home.getId(), camera.getType(), camera.getName(), null);
});
// Discover Known Persons
nonNullStream(home.getPersons()).filter(person -> person.getPseudo() != null).forEach(person -> {
onDeviceAddedInternal(person.getId(), home.getId(), WELCOME_PERSON_THING_TYPE.getId(),
person.getPseudo(), null);
});
}
}
private void onDeviceAddedInternal(String id, @Nullable String parentId, String type, String name,
@Nullable Integer firmwareVersion) {
ThingUID thingUID = findThingUID(type, id);
Map<String, Object> properties = new HashMap<>();
properties.put(EQUIPMENT_ID, id);
if (parentId != null) {
properties.put(PARENT_ID, parentId);
}
if (firmwareVersion != null) {
properties.put(Thing.PROPERTY_VENDOR, VENDOR);
properties.put(Thing.PROPERTY_FIRMWARE_VERSION, firmwareVersion);
properties.put(Thing.PROPERTY_MODEL_ID, type);
properties.put(Thing.PROPERTY_SERIAL_NUMBER, id);
}
addDiscoveredThing(thingUID, properties, name);
}
private void addDiscoveredThing(ThingUID thingUID, Map<String, Object> properties, String displayLabel) {
DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(thingUID).withProperties(properties)
.withBridge(netatmoBridgeHandler.getThing().getUID()).withLabel(displayLabel)
.withRepresentationProperty(EQUIPMENT_ID).build();
thingDiscovered(discoveryResult);
}
private ThingUID findThingUID(String thingType, String thingId) throws IllegalArgumentException {
for (ThingTypeUID supportedThingTypeUID : getSupportedThingTypes()) {
String uid = supportedThingTypeUID.getId();
if (uid.equalsIgnoreCase(thingType)) {
return new ThingUID(supportedThingTypeUID, netatmoBridgeHandler.getThing().getUID(),
thingId.replaceAll("[^a-zA-Z0-9_]", ""));
}
}
throw new IllegalArgumentException("Unsupported device type discovered : " + thingType);
}
private String createWeatherStationName(NAMain station, boolean isFavorite) {
StringBuilder nameBuilder = new StringBuilder();
nameBuilder.append(localizeType(station.getType()));
if (station.getStationName() != null) {
nameBuilder.append(' ');
nameBuilder.append(station.getStationName());
}
if (isFavorite) {
nameBuilder.append(" (favorite)");
}
return nameBuilder.toString();
}
private String createWeatherModuleName(NAMain station, NAStationModule module, boolean isFavorite) {
StringBuilder nameBuilder = new StringBuilder();
if (module.getModuleName() != null) {
nameBuilder.append(module.getModuleName());
} else {
nameBuilder.append(localizeType(module.getType()));
}
if (station.getStationName() != null) {
nameBuilder.append(' ');
nameBuilder.append(station.getStationName());
}
if (isFavorite) {
nameBuilder.append(" (favorite)");
}
return nameBuilder.toString();
}
private String localizeType(String typeName) {
Bundle bundle = FrameworkUtil.getBundle(this.getClass());
@Nullable
String localizedType = i18nProvider.getText(bundle, "thing-type.netatmo." + typeName + ".label", typeName,
localeProvider.getLocale());
if (localizedType != null) {
return localizedType;
}
return typeName;
}
}

View File

@ -1,268 +0,0 @@
/**
* 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;
import static org.openhab.binding.netatmo.internal.NetatmoBindingConstants.*;
import static org.openhab.core.library.unit.MetricPrefix.*;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import javax.measure.Unit;
import javax.measure.quantity.Angle;
import javax.measure.quantity.Dimensionless;
import javax.measure.quantity.Length;
import javax.measure.quantity.Pressure;
import javax.measure.quantity.Speed;
import javax.measure.quantity.Temperature;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.netatmo.internal.channelhelper.BatteryHelper;
import org.openhab.binding.netatmo.internal.channelhelper.RadioHelper;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.i18n.TimeZoneProvider;
import org.openhab.core.library.unit.SIUnits;
import org.openhab.core.library.unit.Units;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.ThingStatusInfo;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.thing.binding.BridgeHandler;
import org.openhab.core.thing.type.ChannelKind;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* {@link AbstractNetatmoThingHandler} is the abstract class that handles
* common behaviors of all netatmo things
*
* @author Gaël L'hopital - Initial contribution OH2 version
* @author Rob Nielsen - Added day, week, and month measurements to the weather station and modules
*
*/
@NonNullByDefault
public abstract class AbstractNetatmoThingHandler extends BaseThingHandler {
// Units of measurement of the data delivered by the API
public static final Unit<Temperature> API_TEMPERATURE_UNIT = SIUnits.CELSIUS;
public static final Unit<Dimensionless> API_HUMIDITY_UNIT = Units.PERCENT;
public static final Unit<Pressure> API_PRESSURE_UNIT = HECTO(SIUnits.PASCAL);
public static final Unit<Speed> API_WIND_SPEED_UNIT = SIUnits.KILOMETRE_PER_HOUR;
public static final Unit<Angle> API_WIND_DIRECTION_UNIT = Units.DEGREE_ANGLE;
public static final Unit<Length> API_RAIN_UNIT = MILLI(SIUnits.METRE);
public static final Unit<Dimensionless> API_CO2_UNIT = Units.PARTS_PER_MILLION;
public static final Unit<Dimensionless> API_NOISE_UNIT = Units.DECIBEL;
private final Logger logger = LoggerFactory.getLogger(AbstractNetatmoThingHandler.class);
protected final TimeZoneProvider timeZoneProvider;
private @Nullable RadioHelper radioHelper;
private @Nullable BatteryHelper batteryHelper;
protected @Nullable Configuration config;
private @Nullable NetatmoBridgeHandler bridgeHandler;
AbstractNetatmoThingHandler(Thing thing, final TimeZoneProvider timeZoneProvider) {
super(thing);
this.timeZoneProvider = timeZoneProvider;
}
@Override
public void initialize() {
logger.debug("initializing handler for thing {}", getThing().getUID());
Bridge bridge = getBridge();
initializeThing(bridge != null ? bridge.getStatus() : null);
}
@Override
public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
logger.debug("bridgeStatusChanged {} for thing {}", bridgeStatusInfo, getThing().getUID());
initializeThing(bridgeStatusInfo.getStatus());
}
private void initializeThing(@Nullable ThingStatus bridgeStatus) {
Bridge bridge = getBridge();
BridgeHandler bridgeHandler = bridge != null ? bridge.getHandler() : null;
if (bridgeHandler != null && bridgeStatus != null) {
if (bridgeStatus == ThingStatus.ONLINE) {
config = getThing().getConfiguration();
String signalLevel = thing.getProperties().get(PROPERTY_SIGNAL_LEVELS);
radioHelper = signalLevel != null ? new RadioHelper(signalLevel) : null;
String batteryLevel = thing.getProperties().get(PROPERTY_BATTERY_LEVELS);
batteryHelper = batteryLevel != null ? new BatteryHelper(batteryLevel) : null;
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "Pending parent object initialization");
initializeThing();
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
}
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
}
}
protected abstract void initializeThing();
protected State getNAThingProperty(String channelId) {
Optional<State> result = getBatteryHelper().flatMap(helper -> helper.getNAThingProperty(channelId));
if (result.isPresent()) {
return result.get();
}
result = getRadioHelper().flatMap(helper -> helper.getNAThingProperty(channelId));
if (result.isPresent()) {
return result.get();
}
return UnDefType.UNDEF;
}
protected void updateChannels() {
if (thing.getStatus() != ThingStatus.ONLINE) {
return;
}
updateDataChannels();
triggerEventChannels();
}
private void updateDataChannels() {
getThing().getChannels().stream()
.filter(channel -> !ChannelKind.TRIGGER.equals(channel.getKind()) && isLinked(channel.getUID()))
.map(channel -> channel.getUID()).forEach(this::updateChannel);
}
private void updateChannel(ChannelUID channelUID) {
updateState(channelUID, getNAThingProperty(channelUID.getId()));
}
/**
* Triggers all event/trigger channels
* (when a channel is triggered, a rule can get all other information from the updated non-trigger channels)
*/
private void triggerEventChannels() {
getThing().getChannels().stream().filter(channel -> ChannelKind.TRIGGER.equals(channel.getKind()))
.map(channel -> channel.getUID().getId()).forEach(this::triggerChannelIfRequired);
}
/**
* Triggers the trigger channel with the given channel id when required (when an update is available)
*
* @param channelId channel id
*/
protected void triggerChannelIfRequired(String channelId) {
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (command == RefreshType.REFRESH) {
logger.debug("Refreshing '{}'", channelUID);
updateChannel(channelUID);
}
}
protected Optional<NetatmoBridgeHandler> getBridgeHandler() {
if (bridgeHandler == null) {
Bridge bridge = getBridge();
if (bridge != null) {
bridgeHandler = (NetatmoBridgeHandler) bridge.getHandler();
}
}
NetatmoBridgeHandler handler = bridgeHandler;
return handler != null ? Optional.of(handler) : Optional.empty();
}
protected Optional<AbstractNetatmoThingHandler> findNAThing(@Nullable String searchedId) {
return getBridgeHandler().flatMap(handler -> handler.findNAThing(searchedId));
}
public boolean matchesId(@Nullable String searchedId) {
return searchedId != null && searchedId.equalsIgnoreCase(getId());
}
protected @Nullable String getId() {
Configuration conf = config;
Object equipmentId = conf != null ? conf.get(EQUIPMENT_ID) : null;
if (equipmentId instanceof String) {
return ((String) equipmentId).toLowerCase();
}
return null;
}
protected void updateProperties(@Nullable Integer firmware, @Nullable String modelId) {
Map<String, String> properties = editProperties();
if (firmware != null || modelId != null) {
properties.put(Thing.PROPERTY_VENDOR, VENDOR);
}
if (firmware != null) {
properties.put(Thing.PROPERTY_FIRMWARE_VERSION, firmware.toString());
}
if (modelId != null) {
properties.put(Thing.PROPERTY_MODEL_ID, modelId);
}
updateProperties(properties);
}
protected Optional<RadioHelper> getRadioHelper() {
RadioHelper helper = radioHelper;
return helper != null ? Optional.of(helper) : Optional.empty();
}
protected Optional<BatteryHelper> getBatteryHelper() {
BatteryHelper helper = batteryHelper;
return helper != null ? Optional.of(helper) : Optional.empty();
}
public void updateMeasurements() {
}
public void getMeasurements(@Nullable String device, @Nullable String module, String scale, List<String> types,
List<String> channels, Map<String, Float> channelMeasurements) {
Optional<NetatmoBridgeHandler> handler = getBridgeHandler();
if (!handler.isPresent() || device == null) {
return;
}
if (types.size() != channels.size()) {
throw new IllegalArgumentException("types and channels lists are different sizes.");
}
List<Float> measurements = handler.get().getStationMeasureResponses(device, module, scale, types);
if (measurements.size() != types.size()) {
throw new IllegalArgumentException("types and measurements lists are different sizes.");
}
int i = 0;
for (Float measurement : measurements) {
channelMeasurements.put(channels.get(i++), measurement);
}
}
public void addMeasurement(List<String> channels, List<String> types, String channel, String type) {
if (isLinked(channel)) {
channels.add(channel);
types.add(type);
}
}
protected boolean isReachable() {
return true;
}
}

View File

@ -0,0 +1,241 @@
/**
* 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;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
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.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpStatus.Code;
import org.openhab.binding.netatmo.internal.api.ApiError;
import org.openhab.binding.netatmo.internal.api.AuthenticationApi;
import org.openhab.binding.netatmo.internal.api.NetatmoException;
import org.openhab.binding.netatmo.internal.api.RestManager;
import org.openhab.binding.netatmo.internal.api.data.NetatmoConstants.Scope;
import org.openhab.binding.netatmo.internal.config.ApiHandlerConfiguration;
import org.openhab.binding.netatmo.internal.config.ApiHandlerConfiguration.Credentials;
import org.openhab.binding.netatmo.internal.config.BindingConfiguration;
import org.openhab.binding.netatmo.internal.deserialization.NADeserializer;
import org.openhab.binding.netatmo.internal.discovery.NetatmoDiscoveryService;
import org.openhab.binding.netatmo.internal.webhook.NetatmoServlet;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseBridgeHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.openhab.core.types.Command;
import org.osgi.service.http.HttpService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* {@link ApiBridgeHandler} is the handler for a Netatmo API and connects it to the framework.
*
* @author Gaël L'hopital - Initial contribution
*
*/
@NonNullByDefault
public class ApiBridgeHandler extends BaseBridgeHandler {
private static final int TIMEOUT_S = 20;
private final Logger logger = LoggerFactory.getLogger(ApiBridgeHandler.class);
private final BindingConfiguration bindingConf;
private final HttpService httpService;
private final AuthenticationApi connectApi;
private final HttpClient httpClient;
private final NADeserializer deserializer;
private Optional<ScheduledFuture<?>> connectJob = Optional.empty();
private Optional<NetatmoServlet> servlet = Optional.empty();
private @NonNullByDefault({}) ApiHandlerConfiguration thingConf;
private Map<Class<? extends RestManager>, RestManager> managers = new HashMap<>();
public ApiBridgeHandler(Bridge bridge, HttpClient httpClient, HttpService httpService, NADeserializer deserializer,
BindingConfiguration configuration) {
super(bridge);
this.bindingConf = configuration;
this.httpService = httpService;
this.connectApi = new AuthenticationApi(this, scheduler);
this.httpClient = httpClient;
this.deserializer = deserializer;
}
@Override
public void initialize() {
logger.debug("Initializing Netatmo API bridge handler.");
thingConf = getConfigAs(ApiHandlerConfiguration.class);
updateStatus(ThingStatus.UNKNOWN);
scheduler.execute(() -> {
openConnection();
String webHookUrl = thingConf.webHookUrl;
if (webHookUrl != null && !webHookUrl.isBlank()) {
servlet = Optional.of(new NetatmoServlet(httpService, this, webHookUrl));
}
});
}
private void openConnection() {
try {
Credentials credentials = thingConf.getCredentials();
logger.debug("Connecting to Netatmo API.");
try {
connectApi.authenticate(credentials, bindingConf.features);
updateStatus(ThingStatus.ONLINE);
getThing().getThings().stream().filter(Thing::isEnabled).map(Thing::getHandler).filter(Objects::nonNull)
.map(CommonInterface.class::cast).forEach(CommonInterface::expireData);
} catch (NetatmoException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
prepareReconnection();
}
} catch (NetatmoException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
}
}
private void prepareReconnection() {
connectApi.disconnect();
freeConnectJob();
connectJob = Optional
.of(scheduler.schedule(() -> openConnection(), thingConf.reconnectInterval, TimeUnit.SECONDS));
}
private void freeConnectJob() {
connectJob.ifPresent(j -> j.cancel(true));
connectJob = Optional.empty();
}
@Override
public void dispose() {
logger.debug("Shutting down Netatmo API bridge handler.");
servlet.ifPresent(servlet -> servlet.dispose());
servlet = Optional.empty();
connectApi.dispose();
freeConnectJob();
super.dispose();
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
logger.debug("Netatmo Bridge is read-only and does not handle commands");
}
@Override
public Collection<Class<? extends ThingHandlerService>> getServices() {
return Set.of(NetatmoDiscoveryService.class);
}
@SuppressWarnings("unchecked")
public <T extends RestManager> @Nullable T getRestManager(Class<T> clazz) {
if (!managers.containsKey(clazz)) {
try {
Constructor<T> constructor = clazz.getConstructor(getClass());
T instance = constructor.newInstance(this);
Set<Scope> expected = instance.getRequiredScopes();
if (connectApi.matchesScopes(expected)) {
managers.put(clazz, instance);
} else {
logger.info("Unable to instantiate {}, expected scope {} is not active", clazz, expected);
}
} catch (SecurityException | ReflectiveOperationException e) {
logger.warn("Error invoking RestManager constructor for class {} : {}", clazz, e.getMessage());
}
}
return (T) managers.get(clazz);
}
public synchronized <T> T executeUri(URI uri, HttpMethod method, Class<T> clazz, @Nullable String payload,
@Nullable String contentType, int retryCount) throws NetatmoException {
try {
logger.trace("executeUri {} {} ", method.toString(), uri);
Request request = httpClient.newRequest(uri).method(method).timeout(TIMEOUT_S, TimeUnit.SECONDS);
String auth = connectApi.getAuthorization();
if (auth != null) {
request.header(HttpHeader.AUTHORIZATION, auth);
}
if (payload != null && contentType != null
&& (HttpMethod.POST.equals(method) || HttpMethod.PUT.equals(method))) {
InputStream stream = new ByteArrayInputStream(payload.getBytes(StandardCharsets.UTF_8));
try (InputStreamContentProvider inputStreamContentProvider = new InputStreamContentProvider(stream)) {
request.content(inputStreamContentProvider, contentType);
}
}
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);
if (statusCode != Code.OK) {
ApiError error = deserializer.deserialize(ApiError.class, responseBody);
throw new NetatmoException(error);
}
return deserializer.deserialize(clazz, responseBody);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
throw new NetatmoException(String.format("%s: \"%s\"", e.getClass().getName(), e.getMessage()));
} catch (TimeoutException | ExecutionException e) {
if (retryCount > 0) {
logger.debug("Request timedout, retry counter : {}", retryCount);
return executeUri(uri, method, clazz, payload, contentType, retryCount - 1);
}
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "@text/request-time-out");
prepareReconnection();
throw new NetatmoException(String.format("%s: \"%s\"", e.getClass().getName(), e.getMessage()));
}
}
public BindingConfiguration getConfiguration() {
return bindingConf;
}
public Optional<NetatmoServlet> getServlet() {
return servlet;
}
public NADeserializer getDeserializer() {
return deserializer;
}
public boolean isConnected() {
return connectApi.isConnected();
}
}

View File

@ -0,0 +1,220 @@
/**
* 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;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;
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.config.NAThingConfiguration;
import org.openhab.binding.netatmo.internal.handler.capability.Capability;
import org.openhab.binding.netatmo.internal.handler.capability.CapabilityMap;
import org.openhab.binding.netatmo.internal.handler.capability.HomeCapability;
import org.openhab.binding.netatmo.internal.handler.capability.RefreshCapability;
import org.openhab.binding.netatmo.internal.handler.capability.RestCapability;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BridgeHandler;
import org.openhab.core.thing.binding.builder.ThingBuilder;
import org.openhab.core.thing.type.ChannelKind;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.State;
import org.slf4j.Logger;
/**
* {@link CommonInterface} defines common methods of AccountHandler and NAThingHandlers used by Capabilities
*
* @author Gaël L'hopital - Initial contribution
*
*/
@NonNullByDefault
public interface CommonInterface {
Thing getThing();
ThingBuilder editThing();
CapabilityMap getCapabilities();
Logger getLogger();
ScheduledExecutorService getScheduler();
boolean isLinked(ChannelUID channelUID);
void updateState(ChannelUID channelUID, State state);
void setThingStatus(ThingStatus thingStatus, ThingStatusDetail thingStatusDetail,
@Nullable String thingStatusReason);
void triggerChannel(String channelID, String event);
void updateThing(Thing thing);
@Nullable
Bridge getBridge();
default @Nullable CommonInterface getBridgeHandler() {
Bridge bridge = getBridge();
return bridge != null && bridge.getHandler() instanceof DeviceHandler ? (DeviceHandler) bridge.getHandler()
: null;
}
default @Nullable ApiBridgeHandler getAccountHandler() {
Bridge bridge = getBridge();
BridgeHandler bridgeHandler = null;
if (bridge != null) {
bridgeHandler = bridge.getHandler();
while (bridgeHandler != null && !(bridgeHandler instanceof ApiBridgeHandler)) {
bridge = ((CommonInterface) bridgeHandler).getBridge();
bridgeHandler = bridge != null ? bridge.getHandler() : null;
}
}
return (ApiBridgeHandler) bridgeHandler;
}
default @Nullable String getBridgeId() {
CommonInterface bridge = getBridgeHandler();
return bridge != null ? bridge.getId() : null;
}
default void expireData() {
getCapabilities().values().forEach(cap -> cap.expireData());
}
default String getId() {
return (String) getThing().getConfiguration().get("id");
}
default Stream<Channel> getActiveChannels() {
return getThing().getChannels().stream()
.filter(channel -> ChannelKind.STATE.equals(channel.getKind()) && isLinked(channel.getUID()));
}
default Optional<CommonInterface> getHomeHandler() {
CommonInterface bridgeHandler = getBridgeHandler();
if (bridgeHandler != null) {
return bridgeHandler.getCapabilities().get(HomeCapability.class).isPresent() ? Optional.of(bridgeHandler)
: Optional.empty();
}
return Optional.empty();
}
default List<CommonInterface> getActiveChildren() {
Thing thing = getThing();
if (thing instanceof Bridge) {
return ((Bridge) thing).getThings().stream().filter(Thing::isEnabled).map(Thing::getHandler)
.filter(Objects::nonNull).map(CommonInterface.class::cast).collect(Collectors.toList());
}
return List.of();
}
default <T extends RestCapability<?>> Optional<T> getHomeCapability(Class<T> clazz) {
return getHomeHandler().map(handler -> handler.getCapabilities().get(clazz)).orElse(Optional.empty());
}
default void setNewData(NAObject newData) {
String finalReason = null;
for (Capability cap : getCapabilities().values()) {
String thingStatusReason = cap.setNewData(newData);
if (thingStatusReason != null) {
finalReason = thingStatusReason;
}
}
if (!newData.isIgnoredForThingUpdate()) {
setThingStatus(finalReason == null ? ThingStatus.ONLINE : ThingStatus.OFFLINE, ThingStatusDetail.NONE,
finalReason);
}
}
default void commonHandleCommand(ChannelUID channelUID, Command command) {
if (ThingStatus.ONLINE.equals(getThing().getStatus())) {
if (command == RefreshType.REFRESH) {
expireData();
return;
}
String channelName = channelUID.getIdWithoutGroup();
getCapabilities().values().forEach(cap -> cap.handleCommand(channelName, command));
} else {
getLogger().debug("Command {}, on channel {} dropped - thing is not ONLINE", command, channelUID);
}
}
default void proceedWithUpdate() {
updateReadings().forEach(dataSet -> setNewData(dataSet));
}
default List<NAObject> updateReadings() {
List<NAObject> result = new ArrayList<>();
getCapabilities().values().forEach(cap -> result.addAll(cap.updateReadings()));
getActiveChildren().forEach(child -> result.addAll(child.updateReadings()));
return result;
}
default void commonInitialize() {
Bridge bridge = getBridge();
if (bridge == null || bridge.getHandler() == null) {
setThingStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED, null);
} else if (!ThingStatus.ONLINE.equals(bridge.getStatus())) {
setThingStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, null);
removeRefreshCapability();
} else {
setThingStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, null);
setRefreshCapability();
getCapabilities().values().forEach(cap -> cap.initialize());
getScheduler().schedule(() -> {
CommonInterface bridgeHandler = getBridgeHandler();
if (bridgeHandler != null) {
bridgeHandler.expireData();
}
}, 1, TimeUnit.SECONDS);
}
}
default void setRefreshCapability() {
ModuleType moduleType = ModuleType.from(getThing().getThingTypeUID());
if (ModuleType.ACCOUNT.equals(moduleType.getBridge())) {
NAThingConfiguration config = getThing().getConfiguration().as(NAThingConfiguration.class);
getCapabilities().put(new RefreshCapability(this, getScheduler(), config.refreshInterval));
}
}
default void removeRefreshCapability() {
Capability refreshCap = getCapabilities().remove(RefreshCapability.class);
if (refreshCap != null) {
refreshCap.dispose();
}
}
default void commonDispose() {
getCapabilities().values().forEach(Capability::dispose);
}
default void removeChannels(List<Channel> channels) {
ThingBuilder builder = editThing().withoutChannels(channels);
updateThing(builder.build());
}
}

View File

@ -0,0 +1,121 @@
/**
* 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;
import java.util.concurrent.ScheduledExecutorService;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.netatmo.internal.handler.capability.CapabilityMap;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.ThingStatusInfo;
import org.openhab.core.thing.binding.BaseBridgeHandler;
import org.openhab.core.thing.binding.builder.BridgeBuilder;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* {@link DeviceHandler} is the base class for all Netatmo bridges
*
* @author Gaël L'hopital - Initial contribution
*
*/
@NonNullByDefault
public class DeviceHandler extends BaseBridgeHandler implements CommonInterface {
private final Logger logger = LoggerFactory.getLogger(DeviceHandler.class);
private CapabilityMap capabilities = new CapabilityMap();
public DeviceHandler(Bridge bridge) {
super(bridge);
}
@Override
public void initialize() {
logger.debug("Initializing handler for bridge {}", getThing().getUID());
commonInitialize();
}
@Override
public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
logger.debug("bridgeStatusChanged for bridge {} to {}", getThing().getUID(), bridgeStatusInfo);
commonInitialize();
}
@Override
public void dispose() {
commonDispose();
super.dispose();
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
commonHandleCommand(channelUID, command);
}
@Override
public void setThingStatus(ThingStatus thingStatus, ThingStatusDetail thingStatusDetail,
@Nullable String thingStatusReason) {
updateStatus(thingStatus, thingStatusDetail, thingStatusReason);
}
@Override
public CapabilityMap getCapabilities() {
return capabilities;
}
@Override
public BridgeBuilder editThing() {
return super.editThing();
}
@Override
public void updateThing(Thing thing) {
super.updateThing(thing);
}
@Override
public void updateState(ChannelUID channelUID, State state) {
super.updateState(channelUID, state);
}
@Override
public boolean isLinked(ChannelUID channelUID) {
return super.isLinked(channelUID);
}
@Override
public @Nullable Bridge getBridge() {
return super.getBridge();
}
@Override
public void triggerChannel(String channelID, String event) {
super.triggerChannel(channelID, event);
}
@Override
public Logger getLogger() {
return logger;
}
@Override
public ScheduledExecutorService getScheduler() {
return scheduler;
}
}

View File

@ -0,0 +1,132 @@
/**
* 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;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.ScheduledExecutorService;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.netatmo.internal.handler.capability.CapabilityMap;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.ThingStatusInfo;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.openhab.core.thing.binding.builder.ThingBuilder;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* {@link ModuleHandler} is the base class for all Netatmo things
*
* @author Gaël L'hopital - Initial contribution
*
*/
@NonNullByDefault
public class ModuleHandler extends BaseThingHandler implements CommonInterface {
private final Logger logger = LoggerFactory.getLogger(ModuleHandler.class);
private CapabilityMap capabilities = new CapabilityMap();
public ModuleHandler(Thing thing) {
super(thing);
}
@Override
public void initialize() {
logger.debug("Initializing handler for thing {}", getThing().getUID());
commonInitialize();
}
@Override
public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
logger.debug("bridgeStatusChanged for thing {} to {}", getThing().getUID(), bridgeStatusInfo);
commonInitialize();
}
@Override
public void dispose() {
commonDispose();
super.dispose();
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
commonHandleCommand(channelUID, command);
}
@Override
public void setThingStatus(ThingStatus thingStatus, ThingStatusDetail thingStatusDetail,
@Nullable String thingStatusReason) {
updateStatus(thingStatus, thingStatusDetail, thingStatusReason);
}
@Override
public CapabilityMap getCapabilities() {
return capabilities;
}
@Override
public ThingBuilder editThing() {
return super.editThing();
}
@Override
public void updateThing(Thing thing) {
super.updateThing(thing);
}
@Override
public void updateState(ChannelUID channelUID, State state) {
super.updateState(channelUID, state);
}
@Override
public boolean isLinked(ChannelUID channelUID) {
return super.isLinked(channelUID);
}
@Override
public @Nullable Bridge getBridge() {
return super.getBridge();
}
@Override
public void triggerChannel(String channelID, String event) {
super.triggerChannel(channelID, event);
}
@Override
public Collection<Class<? extends ThingHandlerService>> getServices() {
List<Class<? extends ThingHandlerService>> result = new ArrayList<>();
capabilities.values().forEach(cap -> result.addAll(cap.getServices()));
return result;
}
@Override
public Logger getLogger() {
return logger;
}
@Override
public ScheduledExecutorService getScheduler() {
return scheduler;
}
}

View File

@ -1,408 +0,0 @@
/**
* 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;
import static org.openhab.binding.netatmo.internal.NetatmoBindingConstants.*;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
import org.apache.oltu.oauth2.client.OAuthClient;
import org.apache.oltu.oauth2.client.URLConnectionClient;
import org.apache.oltu.oauth2.client.request.OAuthClientRequest;
import org.apache.oltu.oauth2.client.response.OAuthJSONAccessTokenResponse;
import org.apache.oltu.oauth2.common.exception.OAuthProblemException;
import org.apache.oltu.oauth2.common.exception.OAuthSystemException;
import org.apache.oltu.oauth2.common.message.types.GrantType;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.netatmo.internal.config.NetatmoBridgeConfiguration;
import org.openhab.binding.netatmo.internal.webhook.NAWebhookCameraEvent;
import org.openhab.binding.netatmo.internal.webhook.NAWebhookCameraEventPerson;
import org.openhab.binding.netatmo.internal.webhook.WelcomeWebHookServlet;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseBridgeHandler;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.swagger.client.ApiClient;
import io.swagger.client.ApiException;
import io.swagger.client.api.HealthyhomecoachApi;
import io.swagger.client.api.PartnerApi;
import io.swagger.client.api.StationApi;
import io.swagger.client.api.ThermostatApi;
import io.swagger.client.api.WelcomeApi;
import io.swagger.client.auth.Authentication;
import io.swagger.client.auth.OAuth;
import io.swagger.client.model.NAHealthyHomeCoachDataBody;
import io.swagger.client.model.NAMeasureBodyElem;
import io.swagger.client.model.NAStationDataBody;
import io.swagger.client.model.NAThermostatDataBody;
import io.swagger.client.model.NAWelcomeHomeData;
/**
* {@link NetatmoBridgeHandler} is the handler for a Netatmo API and connects it
* to the framework. The devices and modules uses the
* {@link NetatmoBridgeHandler} to request informations about their status
*
* @author Gaël L'hopital - Initial contribution OH2 version
* @author Rob Nielsen - Added day, week, and month measurements to the weather station and modules
*
*/
@NonNullByDefault
public class NetatmoBridgeHandler extends BaseBridgeHandler {
private final Logger logger = LoggerFactory.getLogger(NetatmoBridgeHandler.class);
public NetatmoBridgeConfiguration configuration = new NetatmoBridgeConfiguration();
private @Nullable ScheduledFuture<?> refreshJob;
private @Nullable APICreator apiCreator;
private @Nullable WelcomeWebHookServlet webHookServlet;
private List<NetatmoDataListener> dataListeners = new CopyOnWriteArrayList<>();
private static class APICreator {
private final ApiClient apiClient;
private final Map<Class<?>, Object> apiMap;
private APICreator(ApiClient apiClient) {
super();
this.apiClient = apiClient;
apiMap = new HashMap<>();
}
@SuppressWarnings("unchecked")
public <T> T getAPI(Class<T> apiClass) {
T api = (T) apiMap.get(apiClass);
if (api == null) {
try {
api = apiClass.getDeclaredConstructor(ApiClient.class).newInstance(apiClient);
} catch (InstantiationException | IllegalAccessException | InvocationTargetException
| NoSuchMethodException e) {
throw new RuntimeException("Error on executing API class constructor!", e);
}
apiMap.put(apiClass, api);
}
return api;
}
}
public NetatmoBridgeHandler(Bridge bridge, @Nullable WelcomeWebHookServlet webHookServlet) {
super(bridge);
this.webHookServlet = webHookServlet;
}
@Override
public void initialize() {
logger.debug("Initializing Netatmo API bridge handler.");
configuration = getConfigAs(NetatmoBridgeConfiguration.class);
scheduleTokenInitAndRefresh();
}
private void connectionSucceed() {
updateStatus(ThingStatus.ONLINE);
WelcomeWebHookServlet servlet = webHookServlet;
String webHookURI = getWebHookURI();
if (servlet != null && webHookURI != null) {
getWelcomeApi().ifPresent(api -> {
servlet.activate(this);
logger.debug("Setting up Netatmo Welcome WebHook");
api.addwebhook(webHookURI, WEBHOOK_APP);
});
}
}
private void scheduleTokenInitAndRefresh() {
refreshJob = scheduler.scheduleWithFixedDelay(() -> {
logger.debug("Initializing API Connection and scheduling token refresh every {}s",
configuration.reconnectInterval);
try {
initializeApiClient();
// I use a connection to Netatmo API using PartnerAPI to ensure that API is reachable
getPartnerApi().partnerdevices();
connectionSucceed();
} catch (ApiException e) {
switch (e.getCode()) {
case 404: // If no partner station has been associated - likely to happen - we'll have this
// error
// but it means connection to API is OK
connectionSucceed();
break;
case 403: // Forbidden Access maybe too many requests ? Let's wait next cycle
logger.warn("Error 403 while connecting to Netatmo API, will retry in {} s",
configuration.reconnectInterval);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"Netatmo Access Forbidden, will retry in " + configuration.reconnectInterval
+ " seconds.");
break;
default:
if (logger.isDebugEnabled()) {
// we also attach the stack trace
logger.error("Unable to connect Netatmo API : {}", e.getMessage(), e);
} else {
logger.error("Unable to connect Netatmo API : {}", e.getMessage());
}
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Unable to connect Netatmo API : " + e.getLocalizedMessage());
}
} catch (RuntimeException e) {
if (logger.isDebugEnabled()) {
logger.warn("Unable to connect Netatmo API : {}", e.getMessage(), e);
} else {
logger.warn("Unable to connect Netatmo API : {}", e.getMessage());
}
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"Netatmo Access Failed, will retry in " + configuration.reconnectInterval + " seconds.");
}
// We'll do this every x seconds to guaranty token refresh
}, 2, configuration.reconnectInterval, TimeUnit.SECONDS);
}
private void initializeApiClient() {
try {
ApiClient apiClient = new ApiClient();
OAuthClientRequest oAuthRequest = OAuthClientRequest.tokenLocation("https://api.netatmo.net/oauth2/token")
.setClientId(configuration.clientId).setClientSecret(configuration.clientSecret)
.setUsername(configuration.username).setPassword(configuration.password).setScope(getApiScope())
.setGrantType(GrantType.PASSWORD).buildBodyMessage();
OAuthClient oAuthClient = new OAuthClient(new URLConnectionClient());
OAuthJSONAccessTokenResponse accessTokenResponse = oAuthClient.accessToken(oAuthRequest,
OAuthJSONAccessTokenResponse.class);
String accessToken = accessTokenResponse.getAccessToken();
for (Authentication authentication : apiClient.getAuthentications().values()) {
if (authentication instanceof OAuth) {
((OAuth) authentication).setAccessToken(accessToken);
}
}
apiCreator = new APICreator(apiClient);
} catch (OAuthSystemException | OAuthProblemException e) {
throw new RuntimeException("Error on trying to get an access token!", e);
}
}
private String getApiScope() {
List<String> scopes = new ArrayList<>();
if (configuration.readStation) {
scopes.add("read_station");
}
if (configuration.readThermostat) {
scopes.add("read_thermostat");
scopes.add("write_thermostat");
}
if (configuration.readHealthyHomeCoach) {
scopes.add("read_homecoach");
}
if (configuration.readWelcome) {
scopes.add("read_camera");
scopes.add("access_camera");
scopes.add("write_camera");
}
if (configuration.readPresence) {
scopes.add("read_presence");
scopes.add("access_presence");
}
return String.join(" ", scopes);
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
logger.debug("Netatmo Bridge is read-only and does not handle commands");
}
public @Nullable PartnerApi getPartnerApi() {
return apiCreator != null ? apiCreator.getAPI(PartnerApi.class) : null;
}
public Optional<StationApi> getStationApi() {
return apiCreator != null ? Optional.of(apiCreator.getAPI(StationApi.class)) : Optional.empty();
}
public Optional<HealthyhomecoachApi> getHomeCoachApi() {
return apiCreator != null ? Optional.of(apiCreator.getAPI(HealthyhomecoachApi.class)) : Optional.empty();
}
public Optional<ThermostatApi> getThermostatApi() {
return apiCreator != null ? Optional.of(apiCreator.getAPI(ThermostatApi.class)) : Optional.empty();
}
public Optional<WelcomeApi> getWelcomeApi() {
return apiCreator != null ? Optional.of(apiCreator.getAPI(WelcomeApi.class)) : Optional.empty();
}
@Override
public void dispose() {
logger.debug("Running dispose()");
WelcomeWebHookServlet servlet = webHookServlet;
if (servlet != null && getWebHookURI() != null) {
getWelcomeApi().ifPresent(api -> {
logger.debug("Releasing Netatmo Welcome WebHook");
servlet.deactivate();
api.dropwebhook(WEBHOOK_APP);
});
}
ScheduledFuture<?> job = refreshJob;
if (job != null) {
job.cancel(true);
refreshJob = null;
}
}
public Optional<NAStationDataBody> getStationsDataBody(@Nullable String equipmentId) {
Optional<NAStationDataBody> data = getStationApi().map(api -> api.getstationsdata(equipmentId, true).getBody());
updateStatus(ThingStatus.ONLINE);
return data;
}
public List<Float> getStationMeasureResponses(String equipmentId, @Nullable String moduleId, String scale,
List<String> types) {
List<NAMeasureBodyElem> data = getStationApi()
.map(api -> api.getmeasure(equipmentId, scale, types, moduleId, null, "last", 1, true, false).getBody())
.orElse(null);
updateStatus(ThingStatus.ONLINE);
NAMeasureBodyElem element = data != null && !data.isEmpty() ? data.get(0) : null;
return element != null ? element.getValue().get(0) : Collections.emptyList();
}
public Optional<NAHealthyHomeCoachDataBody> getHomecoachDataBody(@Nullable String equipmentId) {
Optional<NAHealthyHomeCoachDataBody> data = getHomeCoachApi()
.map(api -> api.gethomecoachsdata(equipmentId).getBody());
updateStatus(ThingStatus.ONLINE);
return data;
}
public Optional<NAThermostatDataBody> getThermostatsDataBody(@Nullable String equipmentId) {
Optional<NAThermostatDataBody> data = getThermostatApi()
.map(api -> api.getthermostatsdata(equipmentId).getBody());
updateStatus(ThingStatus.ONLINE);
return data;
}
public Optional<NAWelcomeHomeData> getWelcomeDataBody(@Nullable String homeId) {
Optional<NAWelcomeHomeData> data = getWelcomeApi().map(api -> api.gethomedata(homeId, null).getBody());
updateStatus(ThingStatus.ONLINE);
return data;
}
/**
* Returns the Url of the picture
*
* @return Url of the picture or UnDefType.UNDEF
*/
public String getPictureUrl(@Nullable String id, @Nullable String key) {
StringBuilder ret = new StringBuilder();
if (id != null && key != null) {
ret.append(WELCOME_PICTURE_URL).append("?").append(WELCOME_PICTURE_IMAGEID).append("=").append(id)
.append("&").append(WELCOME_PICTURE_KEY).append("=").append(key);
}
return ret.toString();
}
public Optional<AbstractNetatmoThingHandler> findNAThing(@Nullable String searchedId) {
List<Thing> things = getThing().getThings();
Stream<AbstractNetatmoThingHandler> naHandlers = things.stream().map(Thing::getHandler)
.filter(AbstractNetatmoThingHandler.class::isInstance).map(AbstractNetatmoThingHandler.class::cast)
.filter(handler -> handler.matchesId(searchedId));
return naHandlers.findAny();
}
public void webHookEvent(NAWebhookCameraEvent event) {
// This currently the only known event type but I suspect usage can grow in the future...
if (event.getAppType() == NAWebhookCameraEvent.AppTypeEnum.CAMERA) {
Set<AbstractNetatmoThingHandler> modules = new HashSet<>();
if (WELCOME_EVENTS.contains(event.getEventType()) || PRESENCE_EVENTS.contains(event.getEventType())) {
String cameraId = event.getCameraId();
if (cameraId != null) {
Optional<AbstractNetatmoThingHandler> camera = findNAThing(cameraId);
camera.ifPresent(modules::add);
}
}
if (HOME_EVENTS.contains(event.getEventType())) {
String homeId = event.getHomeId();
if (homeId != null) {
Optional<AbstractNetatmoThingHandler> home = findNAThing(homeId);
home.ifPresent(modules::add);
}
}
if (PERSON_EVENTS.contains(event.getEventType())) {
List<NAWebhookCameraEventPerson> persons = event.getPersons();
persons.forEach(person -> {
String personId = person.getId();
if (personId != null) {
Optional<AbstractNetatmoThingHandler> personHandler = findNAThing(personId);
personHandler.ifPresent(modules::add);
}
});
}
modules.forEach(module -> {
Channel channel = module.getThing().getChannel(CHANNEL_WELCOME_HOME_EVENT);
if (channel != null) {
triggerChannel(channel.getUID(), event.getEventType().toString());
}
});
}
}
private @Nullable String getWebHookURI() {
String webHookURI = null;
WelcomeWebHookServlet webHookServlet = this.webHookServlet;
if (configuration.webHookUrl != null && (configuration.readWelcome || configuration.readPresence)
&& webHookServlet != null) {
webHookURI = configuration.webHookUrl + webHookServlet.getPath();
}
return webHookURI;
}
public boolean registerDataListener(NetatmoDataListener dataListener) {
return dataListeners.add(dataListener);
}
public boolean unregisterDataListener(NetatmoDataListener dataListener) {
return dataListeners.remove(dataListener);
}
public void checkForNewThings(Object data) {
for (NetatmoDataListener dataListener : dataListeners) {
dataListener.onDataRefreshed(data);
}
}
}

View File

@ -1,32 +0,0 @@
/**
* 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;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link NetatmoDataListener} allows receiving notification when any netatmo device thing handler
* is getting refreshed data from the netatmo server.
*
* @author Laurent Garnier - Initial contribution
*/
@NonNullByDefault
public interface NetatmoDataListener {
/**
* This method is called just after the thing handler fetched new data from the netatmo server.
*
* @param data the retrieved data.
*/
public void onDataRefreshed(Object data);
}

View File

@ -1,257 +0,0 @@
/**
* 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;
import static org.openhab.binding.netatmo.internal.NetatmoBindingConstants.*;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.netatmo.internal.ChannelTypeUtils;
import org.openhab.binding.netatmo.internal.RefreshStrategy;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.i18n.TimeZoneProvider;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.PointType;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.swagger.client.ApiException;
import io.swagger.client.model.NAPlace;
/**
* {@link NetatmoDeviceHandler} is the handler for a given
* device accessed through the Netatmo Bridge
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public abstract class NetatmoDeviceHandler<DEVICE> extends AbstractNetatmoThingHandler {
private static final int MIN_REFRESH_INTERVAL = 2000;
private static final int DEFAULT_REFRESH_INTERVAL = 300000;
private final Logger logger = LoggerFactory.getLogger(NetatmoDeviceHandler.class);
private final Object updateLock = new Object();
private @Nullable ScheduledFuture<?> refreshJob;
private @Nullable RefreshStrategy refreshStrategy;
private @Nullable DEVICE device;
protected Map<String, Object> childs = new ConcurrentHashMap<>();
public NetatmoDeviceHandler(Thing thing, final TimeZoneProvider timeZoneProvider) {
super(thing, timeZoneProvider);
}
@Override
protected void initializeThing() {
defineRefreshInterval();
updateStatus(ThingStatus.ONLINE);
scheduleRefreshJob();
}
private void scheduleRefreshJob() {
RefreshStrategy strategy = refreshStrategy;
if (strategy == null) {
return;
}
long delay = strategy.nextRunDelayInS();
logger.debug("Scheduling update channel thread in {} s", delay);
refreshJob = scheduler.schedule(() -> {
updateChannels(false);
ScheduledFuture<?> job = refreshJob;
if (job != null) {
job.cancel(false);
refreshJob = null;
}
scheduleRefreshJob();
}, delay, TimeUnit.SECONDS);
}
@Override
public void dispose() {
logger.debug("Running dispose()");
ScheduledFuture<?> job = refreshJob;
if (job != null) {
job.cancel(true);
refreshJob = null;
}
}
protected abstract Optional<DEVICE> updateReadings();
protected void updateProperties(DEVICE deviceData) {
}
@Override
protected void updateChannels() {
updateChannels(true);
}
private void updateChannels(boolean requireDefinedRefreshInterval) {
// Avoid concurrent data readings
synchronized (updateLock) {
RefreshStrategy strategy = refreshStrategy;
if (strategy != null) {
logger.debug("Data aged of {} s", strategy.dataAge() / 1000);
boolean dataOutdated = (requireDefinedRefreshInterval && strategy.isSearchingRefreshInterval()) ? false
: strategy.isDataOutdated();
if (dataOutdated) {
logger.debug("Trying to update channels on device {}", getId());
childs.clear();
Optional<DEVICE> newDeviceReading = Optional.empty();
try {
newDeviceReading = updateReadings();
} catch (ApiException e) {
if (logger.isDebugEnabled()) {
// we also attach the stack trace
logger.error("Unable to connect Netatmo API : {}", e.getMessage(), e);
} else {
logger.error("Unable to connect Netatmo API : {}", e.getMessage());
}
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"Unable to connect Netatmo API : " + e.getLocalizedMessage());
}
if (newDeviceReading.isPresent()) {
logger.debug("Successfully updated device {} readings! Now updating channels", getId());
DEVICE theDevice = newDeviceReading.get();
this.device = theDevice;
updateStatus(isReachable() ? ThingStatus.ONLINE : ThingStatus.OFFLINE);
updateProperties(theDevice);
getDataTimestamp().ifPresent(dataTimeStamp -> {
strategy.setDataTimeStamp(dataTimeStamp, timeZoneProvider.getTimeZone());
});
getRadioHelper().ifPresent(helper -> helper.setModule(theDevice));
getBridgeHandler().ifPresent(handler -> {
handler.checkForNewThings(theDevice);
});
} else {
logger.debug("Failed to update device {} readings! Skip updating channels", getId());
}
// Be sure that all channels for the modules will be updated with refreshed data
childs.forEach((childId, moduleData) -> {
findNAThing(childId).map(NetatmoModuleHandler.class::cast).ifPresent(naChildModule -> {
naChildModule.setRefreshRequired(true);
});
});
} else {
logger.debug("Data still valid for device {}", getId());
}
super.updateChannels();
updateChildModules();
}
}
}
@Override
protected State getNAThingProperty(String channelId) {
try {
Optional<DEVICE> dev = getDevice();
switch (channelId) {
case CHANNEL_LAST_STATUS_STORE:
if (dev.isPresent()) {
Method getLastStatusStore = dev.get().getClass().getMethod("getLastStatusStore");
Integer lastStatusStore = (Integer) getLastStatusStore.invoke(dev.get());
return ChannelTypeUtils.toDateTimeType(lastStatusStore, timeZoneProvider.getTimeZone());
} else {
return UnDefType.UNDEF;
}
case CHANNEL_LOCATION:
if (dev.isPresent()) {
Method getPlace = dev.get().getClass().getMethod("getPlace");
NAPlace place = (NAPlace) getPlace.invoke(dev.get());
PointType point = new PointType(new DecimalType(place.getLocation().get(1)),
new DecimalType(place.getLocation().get(0)));
if (place.getAltitude() != null) {
point.setAltitude(new DecimalType(place.getAltitude()));
}
return point;
} else {
return UnDefType.UNDEF;
}
}
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
logger.debug("The device has no method to access {} property ", channelId);
return UnDefType.NULL;
}
return super.getNAThingProperty(channelId);
}
private void updateChildModules() {
logger.debug("Updating child modules of {}", getId());
childs.forEach((childId, moduleData) -> {
findNAThing(childId).map(NetatmoModuleHandler.class::cast).ifPresent(naChildModule -> {
logger.debug("Updating child module {}", naChildModule.getId());
naChildModule.updateChannels(moduleData);
});
});
}
/*
* Sets the refresh rate of the device depending whether it's a property
* of the thing or if it's defined by configuration
*/
private void defineRefreshInterval() {
BigDecimal dataValidityPeriod;
if (thing.getProperties().containsKey(PROPERTY_REFRESH_PERIOD)) {
String refreshPeriodProperty = thing.getProperties().get(PROPERTY_REFRESH_PERIOD);
if ("auto".equalsIgnoreCase(refreshPeriodProperty)) {
dataValidityPeriod = new BigDecimal(-1);
} else {
dataValidityPeriod = new BigDecimal(refreshPeriodProperty);
}
} else {
Configuration conf = config;
Object interval = conf != null ? conf.get(REFRESH_INTERVAL) : null;
if (interval instanceof BigDecimal) {
dataValidityPeriod = (BigDecimal) interval;
if (dataValidityPeriod.intValue() < MIN_REFRESH_INTERVAL) {
logger.info(
"Refresh interval setting is too small for thing {}, {} ms is considered as refresh interval.",
thing.getUID(), MIN_REFRESH_INTERVAL);
dataValidityPeriod = new BigDecimal(MIN_REFRESH_INTERVAL);
}
} else {
dataValidityPeriod = new BigDecimal(DEFAULT_REFRESH_INTERVAL);
}
}
refreshStrategy = new RefreshStrategy(dataValidityPeriod.intValue());
}
protected abstract Optional<Integer> getDataTimestamp();
public void expireData() {
RefreshStrategy strategy = refreshStrategy;
if (strategy != null) {
strategy.expireData();
}
}
protected Optional<DEVICE> getDevice() {
return Optional.ofNullable(device);
}
}

View File

@ -1,145 +0,0 @@
/**
* 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;
import static org.openhab.binding.netatmo.internal.NetatmoBindingConstants.*;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Optional;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.netatmo.internal.ChannelTypeUtils;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.i18n.TimeZoneProvider;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* {@link NetatmoModuleHandler} is the handler for a given
* module device accessed through the Netatmo Device
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class NetatmoModuleHandler<MODULE> extends AbstractNetatmoThingHandler {
private final Logger logger = LoggerFactory.getLogger(NetatmoModuleHandler.class);
private @Nullable ScheduledFuture<?> refreshJob;
private @Nullable MODULE module;
private boolean refreshRequired;
protected NetatmoModuleHandler(Thing thing, final TimeZoneProvider timeZoneProvider) {
super(thing, timeZoneProvider);
}
@Override
protected void initializeThing() {
refreshJob = scheduler.schedule(() -> {
requestParentRefresh();
}, 5, TimeUnit.SECONDS);
}
@Override
public void dispose() {
ScheduledFuture<?> job = refreshJob;
if (job != null) {
job.cancel(true);
refreshJob = null;
}
}
protected @Nullable String getParentId() {
Configuration conf = config;
Object parentId = conf != null ? conf.get(PARENT_ID) : null;
if (parentId instanceof String) {
return ((String) parentId).toLowerCase();
}
return null;
}
public boolean childOf(AbstractNetatmoThingHandler naThingHandler) {
return naThingHandler.matchesId(getParentId());
}
@Override
protected State getNAThingProperty(String channelId) {
try {
Optional<MODULE> mod = getModule();
if (channelId.equalsIgnoreCase(CHANNEL_LAST_MESSAGE) && mod.isPresent()) {
Method getLastMessage = mod.get().getClass().getMethod("getLastMessage");
Integer lastMessage = (Integer) getLastMessage.invoke(mod.get());
return ChannelTypeUtils.toDateTimeType(lastMessage, timeZoneProvider.getTimeZone());
}
} catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException
| InvocationTargetException e) {
logger.debug("The module has no method to access {} property ", channelId);
return UnDefType.NULL;
}
return super.getNAThingProperty(channelId);
}
protected void updateChannels(Object module) {
MODULE theModule = (MODULE) module;
setModule(theModule);
updateStatus(isReachable() ? ThingStatus.ONLINE : ThingStatus.OFFLINE);
getRadioHelper().ifPresent(helper -> helper.setModule(module));
getBatteryHelper().ifPresent(helper -> helper.setModule(module));
updateProperties(theModule);
super.updateChannels();
}
protected void invalidateParentCacheAndRefresh() {
setRefreshRequired(true);
// Leave a bit of time to Netatmo Server to get in sync with new values sent
scheduler.schedule(() -> {
invalidateParentCache();
requestParentRefresh();
}, 2, TimeUnit.SECONDS);
}
protected void requestParentRefresh() {
setRefreshRequired(true);
findNAThing(getParentId()).ifPresent(AbstractNetatmoThingHandler::updateChannels);
}
private void invalidateParentCache() {
findNAThing(getParentId()).map(NetatmoDeviceHandler.class::cast).ifPresent(NetatmoDeviceHandler::expireData);
}
protected void updateProperties(MODULE moduleData) {
}
protected boolean isRefreshRequired() {
return refreshRequired;
}
protected void setRefreshRequired(boolean refreshRequired) {
this.refreshRequired = refreshRequired;
}
protected Optional<MODULE> getModule() {
return Optional.ofNullable(module);
}
public void setModule(MODULE module) {
this.module = module;
}
}

View File

@ -0,0 +1,48 @@
/**
* 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.capability;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.netatmo.internal.api.AircareApi;
import org.openhab.binding.netatmo.internal.api.NetatmoException;
import org.openhab.binding.netatmo.internal.api.dto.NAObject;
import org.openhab.binding.netatmo.internal.handler.CommonInterface;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* {@link AirCareCapability} give the ability to read home coach api
*
* @author Gaël L'hopital - Initial contribution
*
*/
@NonNullByDefault
public class AirCareCapability extends RestCapability<AircareApi> {
private final Logger logger = LoggerFactory.getLogger(AirCareCapability.class);
public AirCareCapability(CommonInterface handler) {
super(handler, AircareApi.class);
}
@Override
protected List<NAObject> updateReadings(AircareApi api) {
try {
return List.of(api.getHomeCoach(handler.getId()));
} catch (NetatmoException e) {
logger.warn("Error retrieving home-coach data '{}' : {}", handler.getId(), e.getMessage());
}
return List.of();
}
}

View File

@ -0,0 +1,107 @@
/**
* 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.capability;
import static org.openhab.binding.netatmo.internal.NetatmoBindingConstants.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
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.AlimentationStatus;
import org.openhab.binding.netatmo.internal.api.data.NetatmoConstants.SdCardStatus;
import org.openhab.binding.netatmo.internal.api.dto.HomeDataPerson;
import org.openhab.binding.netatmo.internal.api.dto.HomeEvent;
import org.openhab.binding.netatmo.internal.api.dto.HomeStatusModule;
import org.openhab.binding.netatmo.internal.api.dto.NAObject;
import org.openhab.binding.netatmo.internal.deserialization.NAObjectMap;
import org.openhab.binding.netatmo.internal.handler.CommonInterface;
import org.openhab.binding.netatmo.internal.handler.channelhelper.CameraChannelHelper;
import org.openhab.binding.netatmo.internal.handler.channelhelper.ChannelHelper;
import org.openhab.binding.netatmo.internal.providers.NetatmoDescriptionProvider;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.StateOption;
/**
* {@link CameraCapability} give to handle Welcome Camera specifics
*
* @author Gaël L'hopital - Initial contribution
*
*/
@NonNullByDefault
public class CameraCapability extends HomeSecurityThingCapability {
private final CameraChannelHelper cameraHelper;
private final ChannelUID personChannelUID;
protected @Nullable String localUrl;
public CameraCapability(CommonInterface handler, NetatmoDescriptionProvider descriptionProvider,
List<ChannelHelper> channelHelpers) {
super(handler, descriptionProvider, channelHelpers);
this.personChannelUID = new ChannelUID(thing.getUID(), GROUP_LAST_EVENT, CHANNEL_EVENT_PERSON_ID);
this.cameraHelper = (CameraChannelHelper) channelHelpers.stream().filter(c -> c instanceof CameraChannelHelper)
.findFirst().orElseThrow(() -> new IllegalArgumentException(
"CameraCapability must find a CameraChannelHelper, please file a bug report."));
}
@Override
public void updateHomeStatusModule(HomeStatusModule newData) {
super.updateHomeStatusModule(newData);
String vpnUrl = newData.getVpnUrl();
if (vpnUrl != null) {
localUrl = newData.isLocal() ? securityCapability.map(cap -> cap.ping(vpnUrl)).orElse(null) : null;
cameraHelper.setUrls(vpnUrl, localUrl);
eventHelper.setUrls(vpnUrl, localUrl);
}
if (!SdCardStatus.SD_CARD_WORKING.equals(newData.getSdStatus())
|| !AlimentationStatus.ALIM_CORRECT_POWER.equals(newData.getAlimStatus())) {
statusReason = String.format("%s, %s", newData.getSdStatus(), newData.getAlimStatus());
}
}
@Override
public void handleCommand(String channelName, Command command) {
if (command instanceof OnOffType && CHANNEL_MONITORING.equals(channelName)) {
securityCapability.ifPresent(cap -> cap.changeStatus(localUrl, OnOffType.ON.equals(command)));
} else {
super.handleCommand(channelName, command);
}
}
@Override
protected void beforeNewData() {
super.beforeNewData();
homeCapability.ifPresent(cap -> {
NAObjectMap<HomeDataPerson> persons = cap.getPersons();
descriptionProvider.setStateOptions(personChannelUID, persons.values().stream()
.map(p -> new StateOption(p.getId(), p.getName())).collect(Collectors.toList()));
});
}
@Override
public List<NAObject> updateReadings() {
List<NAObject> result = new ArrayList<>();
securityCapability.ifPresent(cap -> {
Collection<HomeEvent> events = cap.getCameraEvents(handler.getId());
if (!events.isEmpty()) {
result.add(events.iterator().next());
}
});
return result;
}
}

View File

@ -0,0 +1,175 @@
/**
* 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.capability;
import static org.openhab.binding.netatmo.internal.NetatmoBindingConstants.VENDOR;
import static org.openhab.core.thing.Thing.*;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
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.Device;
import org.openhab.binding.netatmo.internal.api.dto.Event;
import org.openhab.binding.netatmo.internal.api.dto.HomeData;
import org.openhab.binding.netatmo.internal.api.dto.HomeEvent;
import org.openhab.binding.netatmo.internal.api.dto.HomeStatusModule;
import org.openhab.binding.netatmo.internal.api.dto.NAHomeStatus.HomeStatus;
import org.openhab.binding.netatmo.internal.api.dto.NAMain;
import org.openhab.binding.netatmo.internal.api.dto.NAObject;
import org.openhab.binding.netatmo.internal.api.dto.NAThing;
import org.openhab.binding.netatmo.internal.handler.CommonInterface;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.openhab.core.types.Command;
/**
* The {@link Capability} is the base class for all inherited capabilities
*
* @author Gaël L'hopital - Initial contribution
*
*/
@NonNullByDefault
public class Capability {
protected final Thing thing;
protected final CommonInterface handler;
protected final ModuleType moduleType;
protected boolean firstLaunch;
protected Map<String, String> properties = Map.of();
protected @Nullable String statusReason;
Capability(CommonInterface handler) {
this.handler = handler;
this.thing = handler.getThing();
this.moduleType = ModuleType.from(thing.getThingTypeUID());
}
public final @Nullable String setNewData(NAObject newData) {
beforeNewData();
if (newData instanceof HomeData) {
updateHomeData((HomeData) newData);
}
if (newData instanceof HomeStatus) {
updateHomeStatus((HomeStatus) newData);
}
if (newData instanceof HomeStatusModule) {
updateHomeStatusModule((HomeStatusModule) newData);
}
if (newData instanceof Event) {
updateEvent((Event) newData);
}
if (newData instanceof HomeEvent) {
updateHomeEvent((HomeEvent) newData);
}
if (newData instanceof NAThing) {
updateNAThing((NAThing) newData);
}
if (newData instanceof NAMain) {
updateNAMain((NAMain) newData);
}
if (newData instanceof Device) {
updateNADevice((Device) newData);
}
afterNewData(newData);
return statusReason;
}
protected void beforeNewData() {
properties = new HashMap<>(thing.getProperties());
firstLaunch = properties.isEmpty();
if (firstLaunch && !moduleType.isLogical()) {
properties.put(PROPERTY_VENDOR, VENDOR);
properties.put(PROPERTY_MODEL_ID, moduleType.name());
}
statusReason = null;
}
protected void afterNewData(@Nullable NAObject newData) {
if (!properties.equals(thing.getProperties())) {
thing.setProperties(properties);
}
}
protected void updateNAThing(NAThing newData) {
String firmware = newData.getFirmware();
if (firmware != null && !firmware.isBlank()) {
properties.put(PROPERTY_FIRMWARE_VERSION, firmware);
}
if (!newData.isReachable()) {
statusReason = "@text/device-not-connected";
}
}
protected void updateNAMain(NAMain newData) {
// do nothing by default, can be overridden by subclasses
}
protected void updateHomeEvent(HomeEvent newData) {
// do nothing by default, can be overridden by subclasses
}
protected void updateHomeStatus(HomeStatus newData) {
// do nothing by default, can be overridden by subclasses
}
protected void updateHomeData(HomeData newData) {
// do nothing by default, can be overridden by subclasses
}
protected void updateEvent(Event newData) {
// do nothing by default, can be overridden by subclasses
}
protected void updateNADevice(Device newData) {
// do nothing by default, can be overridden by subclasses
}
public void initialize() {
// do nothing by default, can be overridden by subclasses
}
public void expireData() {
if (!handler.getCapabilities().containsKey(RefreshCapability.class)) {
CommonInterface bridgeHandler = handler.getBridgeHandler();
if (bridgeHandler != null) {
bridgeHandler.expireData();
}
}
}
public void dispose() {
// do nothing by default, can be overridden by subclasses
}
public void updateHomeStatusModule(HomeStatusModule newData) {
// do nothing by default, can be overridden by subclasses
}
public void handleCommand(String channelName, Command command) {
// do nothing by default, can be overridden by subclasses
}
public Collection<Class<? extends ThingHandlerService>> getServices() {
return List.of();
}
public List<NAObject> updateReadings() {
return List.of();
}
}

View File

@ -0,0 +1,42 @@
/**
* 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.capability;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* {@link CapabilityMap} is a specialized Map designed to store capabilities
*
* @author Gaël L'hopital - Initial contribution
*
*/
@NonNullByDefault
public class CapabilityMap extends ConcurrentHashMap<Class<?>, Capability> {
private static final long serialVersionUID = -3043492242108419801L;
public void put(Capability capability) {
Class<? extends Capability> clazz = capability.getClass();
if (super.get(clazz) == null) {
super.put(clazz, capability);
}
}
public <T extends Capability> Optional<T> get(Class<T> clazz) {
@SuppressWarnings("unchecked")
T cap = (T) super.get(clazz);
return Optional.ofNullable(cap);
}
}

View File

@ -0,0 +1,59 @@
/**
* 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.capability;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
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.core.config.core.Configuration;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.types.State;
/**
* {@link ChannelHelperCapability} give the capability to dispatch incoming data across the channel helpers.
*
* @author Gaël L'hopital - Initial contribution
*
*/
@NonNullByDefault
public class ChannelHelperCapability extends Capability {
private final List<ChannelHelper> channelHelpers;
public ChannelHelperCapability(CommonInterface handler, List<ChannelHelper> channelHelpers) {
super(handler);
this.channelHelpers = channelHelpers;
}
@Override
public void afterNewData(@Nullable NAObject newData) {
super.afterNewData(newData);
channelHelpers.forEach(helper -> helper.setNewData(newData));
handler.getActiveChannels().forEach(channel -> {
ChannelUID channelUID = channel.getUID();
String channelID = channelUID.getIdWithoutGroup();
String groupId = channelUID.getGroupId();
Configuration channelConfig = channel.getConfiguration();
for (ChannelHelper helper : channelHelpers) {
State state = helper.getChannelState(channelID, groupId, channelConfig);
if (state != null) {
handler.updateState(channelUID, state);
break;
}
}
});
}
}

View File

@ -0,0 +1,48 @@
/**
* 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.capability;
import static org.openhab.binding.netatmo.internal.NetatmoBindingConstants.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.netatmo.internal.api.dto.NAMain;
import org.openhab.binding.netatmo.internal.handler.CommonInterface;
/**
* The {@link DeviceCapability} takes care of handling properties for netatmo devices
*
* @author Gaël L'hopital - Initial contribution
*
*/
@NonNullByDefault
public class DeviceCapability extends Capability {
private static final int DATA_AGE_LIMIT_S = 3600;
public DeviceCapability(CommonInterface handler) {
super(handler);
}
@Override
protected void updateNAMain(NAMain newData) {
if (firstLaunch) {
newData.getPlace().ifPresent(place -> {
place.getCity().map(city -> properties.put(PROPERTY_CITY, city));
place.getCountry().map(country -> properties.put(PROPERTY_COUNTRY, country));
place.getTimezone().map(tz -> properties.put(PROPERTY_TIMEZONE, tz));
});
}
if (!newData.hasFreshData(DATA_AGE_LIMIT_S)) {
statusReason = "@text/data-over-limit";
}
}
}

View File

@ -0,0 +1,155 @@
/**
* 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.capability;
import static org.openhab.binding.netatmo.internal.NetatmoBindingConstants.*;
import java.time.ZonedDateTime;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.netatmo.internal.api.EnergyApi;
import org.openhab.binding.netatmo.internal.api.NetatmoException;
import org.openhab.binding.netatmo.internal.api.data.NetatmoConstants.SetpointMode;
import org.openhab.binding.netatmo.internal.api.dto.HomeData;
import org.openhab.binding.netatmo.internal.api.dto.HomeDataModule;
import org.openhab.binding.netatmo.internal.api.dto.HomeDataRoom;
import org.openhab.binding.netatmo.internal.api.dto.HomeStatusModule;
import org.openhab.binding.netatmo.internal.api.dto.NAHomeStatus.HomeStatus;
import org.openhab.binding.netatmo.internal.api.dto.Room;
import org.openhab.binding.netatmo.internal.deserialization.NAObjectMap;
import org.openhab.binding.netatmo.internal.handler.CommonInterface;
import org.openhab.binding.netatmo.internal.providers.NetatmoDescriptionProvider;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.StateOption;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link EnergyCapability} is the base class for handler able to handle energy features
*
* @author Gaël L'hopital - Initial contribution
*
*/
@NonNullByDefault
public class EnergyCapability extends RestCapability<EnergyApi> {
private final Logger logger = LoggerFactory.getLogger(EnergyCapability.class);
private int setPointDefaultDuration = -1;
private final NetatmoDescriptionProvider descriptionProvider;
EnergyCapability(CommonInterface handler, NetatmoDescriptionProvider descriptionProvider) {
super(handler, EnergyApi.class);
this.descriptionProvider = descriptionProvider;
}
@Override
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) {
roomData.setIgnoredForThingUpdate(true);
handler.setNewData(roomData);
}
HomeDataModule moduleData = modules.get(handler.getId());
if (moduleData != null) {
moduleData.setIgnoredForThingUpdate(true);
handler.setNewData(moduleData);
}
});
descriptionProvider.setStateOptions(new ChannelUID(thing.getUID(), GROUP_ENERGY, CHANNEL_PLANNING),
homeData.getThermSchedules().stream().map(p -> new StateOption(p.getId(), p.getName()))
.collect(Collectors.toList()));
setPointDefaultDuration = homeData.getThermSetpointDefaultDuration();
}
@Override
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);
}
});
}
public int getSetpointDefaultDuration() {
return setPointDefaultDuration;
}
public void setRoomThermMode(String roomId, SetpointMode targetMode) {
getApi().ifPresent(api -> {
try {
api.setThermpoint(handler.getId(), roomId, targetMode,
targetMode == SetpointMode.MAX ? setpointEndTimeFromNow(setPointDefaultDuration) : 0, 0);
handler.expireData();
} catch (NetatmoException e) {
logger.warn("Error setting room thermostat mode '{}' : {}", targetMode, e.getMessage());
}
});
}
public void setRoomThermTemp(String roomId, double temperature, long endtime, SetpointMode mode) {
getApi().ifPresent(api -> {
try {
api.setThermpoint(handler.getId(), roomId, mode, endtime, temperature);
handler.expireData();
} catch (NetatmoException e) {
logger.warn("Error setting room thermostat mode '{}' : {}", mode, e.getMessage());
}
});
}
public void setRoomThermTemp(String roomId, double temperature) {
setRoomThermTemp(roomId, temperature, setpointEndTimeFromNow(setPointDefaultDuration), SetpointMode.MANUAL);
}
@Override
public void handleCommand(String channelName, Command command) {
getApi().ifPresent(api -> {
try {
switch (channelName) {
case CHANNEL_PLANNING:
api.switchSchedule(handler.getId(), command.toString());
break;
case CHANNEL_SETPOINT_MODE:
SetpointMode targetMode = SetpointMode.valueOf(command.toString());
if (targetMode == SetpointMode.MANUAL) {
logger.info("Switch to 'Manual' is done by setting a setpoint temp, command ignored");
return;
}
api.setThermMode(handler.getId(), targetMode.apiDescriptor);
break;
}
handler.expireData();
} catch (NetatmoException e) {
logger.warn("Error handling command '{}' : {}", command, e.getMessage());
} catch (IllegalArgumentException e) {
logger.warn("Command '{}' sent to channel '{}' is not a valid setpoint mode.", command, channelName);
}
});
}
private static long setpointEndTimeFromNow(int duration_min) {
return ZonedDateTime.now().plusMinutes(duration_min).toEpochSecond();
}
}

View File

@ -0,0 +1,51 @@
/**
* 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.capability;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.netatmo.internal.handler.ApiBridgeHandler;
import org.openhab.binding.netatmo.internal.handler.CommonInterface;
import org.openhab.binding.netatmo.internal.webhook.NetatmoServlet;
/**
* {@link EventCapability} is the base class for handlers
* subject to receive event notifications. This class registers to webhookservlet so
* it can be notified when an event arrives.
*
* @author Gaël L'hopital - Initial contribution
*
*/
@NonNullByDefault
public class EventCapability extends Capability {
private Optional<NetatmoServlet> servlet = Optional.empty();
public EventCapability(CommonInterface handler) {
super(handler);
}
@Override
public void initialize() {
ApiBridgeHandler accountHandler = handler.getAccountHandler();
if (accountHandler != null) {
servlet = accountHandler.getServlet();
servlet.ifPresent(s -> s.registerDataListener(handler.getId(), this));
}
}
@Override
public void dispose() {
servlet.ifPresent(s -> s.unregisterDataListener(this));
}
}

View File

@ -0,0 +1,120 @@
/**
* 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.capability;
import static org.openhab.binding.netatmo.internal.NetatmoBindingConstants.*;
import java.util.ArrayList;
import java.util.List;
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.HomeApi;
import org.openhab.binding.netatmo.internal.api.NetatmoException;
import org.openhab.binding.netatmo.internal.api.data.NetatmoConstants.FeatureArea;
import org.openhab.binding.netatmo.internal.api.dto.HomeData;
import org.openhab.binding.netatmo.internal.api.dto.HomeDataModule;
import org.openhab.binding.netatmo.internal.api.dto.HomeDataPerson;
import org.openhab.binding.netatmo.internal.api.dto.Location;
import org.openhab.binding.netatmo.internal.api.dto.NAHomeStatus.HomeStatus;
import org.openhab.binding.netatmo.internal.api.dto.NAObject;
import org.openhab.binding.netatmo.internal.deserialization.NAObjectMap;
import org.openhab.binding.netatmo.internal.handler.CommonInterface;
import org.openhab.binding.netatmo.internal.providers.NetatmoDescriptionProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link HomeCapability} is the base class for handler able to manage persons and modules
*
* @author Gaël L'hopital - Initial contribution
*
*/
@NonNullByDefault
public class HomeCapability extends RestCapability<HomeApi> {
private final Logger logger = LoggerFactory.getLogger(HomeCapability.class);
private final NetatmoDescriptionProvider descriptionProvider;
private NAObjectMap<HomeDataPerson> persons = new NAObjectMap<>();
private NAObjectMap<HomeDataModule> modules = new NAObjectMap<>();
private Set<FeatureArea> featuresArea = Set.of();
public HomeCapability(CommonInterface handler, NetatmoDescriptionProvider descriptionProvider) {
super(handler, HomeApi.class);
this.descriptionProvider = descriptionProvider;
}
@Override
protected void updateHomeData(HomeData home) {
featuresArea = home.getFeatures();
if (hasFeature(FeatureArea.SECURITY) && !handler.getCapabilities().containsKey(SecurityCapability.class)) {
handler.getCapabilities().put(new SecurityCapability(handler));
}
if (hasFeature(FeatureArea.ENERGY) && !handler.getCapabilities().containsKey(EnergyCapability.class)) {
handler.getCapabilities().put(new EnergyCapability(handler, descriptionProvider));
}
if (firstLaunch) {
home.getCountry().map(country -> properties.put(PROPERTY_COUNTRY, country));
home.getTimezone().map(tz -> properties.put(PROPERTY_TIMEZONE, tz));
properties.put(GROUP_LOCATION, ((Location) home).getLocation().toString());
properties.put(PROPERTY_FEATURE, featuresArea.stream().map(f -> f.name()).collect(Collectors.joining(",")));
}
}
@Override
protected void afterNewData(@Nullable NAObject newData) {
super.afterNewData(newData);
if (firstLaunch && !hasFeature(FeatureArea.SECURITY)) {
handler.removeChannels(thing.getChannelsOfGroup(GROUP_SECURITY));
}
if (firstLaunch && !hasFeature(FeatureArea.ENERGY)) {
handler.removeChannels(thing.getChannelsOfGroup(GROUP_ENERGY));
}
}
private boolean hasFeature(FeatureArea seeked) {
return featuresArea.contains(seeked);
}
public NAObjectMap<HomeDataPerson> getPersons() {
return persons;
}
public NAObjectMap<HomeDataModule> getModules() {
return modules;
}
@Override
protected List<NAObject> updateReadings(HomeApi api) {
List<NAObject> result = new ArrayList<>();
try {
HomeData homeData = api.getHomeData(handler.getId());
if (homeData != null) {
result.add(homeData);
persons = homeData.getPersons();
modules = homeData.getModules();
}
HomeStatus homeStatus = api.getHomeStatus(handler.getId());
if (homeStatus != null) {
result.add(homeStatus);
}
} catch (NetatmoException e) {
logger.warn("Error gettting Home informations : {}", e.getMessage());
}
return result;
}
}

View File

@ -0,0 +1,55 @@
/**
* 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.capability;
import java.util.List;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.netatmo.internal.handler.CommonInterface;
import org.openhab.binding.netatmo.internal.handler.channelhelper.ChannelHelper;
import org.openhab.binding.netatmo.internal.handler.channelhelper.EventChannelHelper;
import org.openhab.binding.netatmo.internal.providers.NetatmoDescriptionProvider;
/**
* {@link HomeSecurityThingCapability} is the ancestor of capabilities hosted by a security home
* e.g. person and camera capabilities
*
* @author Gaël L'hopital - Initial contribution
*
*/
@NonNullByDefault
public class HomeSecurityThingCapability extends Capability {
protected final NetatmoDescriptionProvider descriptionProvider;
protected final EventChannelHelper eventHelper;
protected Optional<SecurityCapability> securityCapability = Optional.empty();
protected Optional<HomeCapability> homeCapability = Optional.empty();
public HomeSecurityThingCapability(CommonInterface handler, NetatmoDescriptionProvider descriptionProvider,
List<ChannelHelper> channelHelpers) {
super(handler);
this.descriptionProvider = descriptionProvider;
this.eventHelper = (EventChannelHelper) channelHelpers.stream().filter(c -> c instanceof EventChannelHelper)
.findFirst().orElseThrow(() -> new IllegalArgumentException(
"HomeSecurityThingCapability must find an EventChannelHelper, please file a bug report."));
eventHelper.setModuleType(moduleType);
}
@Override
public void initialize() {
super.initialize();
securityCapability = handler.getHomeCapability(SecurityCapability.class);
homeCapability = handler.getHomeCapability(HomeCapability.class);
}
}

View File

@ -0,0 +1,95 @@
/**
* 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.capability;
import static org.openhab.binding.netatmo.internal.utils.ChannelTypeUtils.*;
import java.time.ZonedDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
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.WeatherApi;
import org.openhab.binding.netatmo.internal.api.data.NetatmoConstants.MeasureClass;
import org.openhab.binding.netatmo.internal.api.dto.NAObject;
import org.openhab.binding.netatmo.internal.config.MeasureConfiguration;
import org.openhab.binding.netatmo.internal.handler.CommonInterface;
import org.openhab.binding.netatmo.internal.handler.channelhelper.ChannelHelper;
import org.openhab.binding.netatmo.internal.handler.channelhelper.MeasuresChannelHelper;
import org.openhab.core.thing.type.ChannelTypeUID;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link MeasureCapability} is the base class for handler able to handle user defined measures
*
* @author Gaël L'hopital - Initial contribution
*
*/
@NonNullByDefault
public class MeasureCapability extends RestCapability<WeatherApi> {
private final Logger logger = LoggerFactory.getLogger(MeasureCapability.class);
private final Map<String, State> measures = new HashMap<>();
public MeasureCapability(CommonInterface handler, List<ChannelHelper> helpers) {
super(handler, WeatherApi.class);
MeasuresChannelHelper measureChannelHelper = (MeasuresChannelHelper) helpers.stream()
.filter(c -> c instanceof MeasuresChannelHelper).findFirst()
.orElseThrow(() -> new IllegalArgumentException(
"MeasureCapability must find a MeasuresChannelHelper, please file a bug report."));
measureChannelHelper.setMeasures(measures);
}
@Override
public List<NAObject> updateReadings(WeatherApi api) {
String bridgeId = handler.getBridgeId();
String deviceId = bridgeId != null ? bridgeId : handler.getId();
String moduleId = bridgeId != null ? handler.getId() : null;
updateMeasures(api, deviceId, moduleId);
return List.of();
}
private void updateMeasures(WeatherApi api, String deviceId, @Nullable String moduleId) {
measures.clear();
handler.getActiveChannels().filter(channel -> !channel.getConfiguration().getProperties().isEmpty())
.forEach(channel -> {
ChannelTypeUID channelTypeUID = channel.getChannelTypeUID();
if (channelTypeUID != null) {
MeasureConfiguration measureDef = channel.getConfiguration().as(MeasureConfiguration.class);
String descriptor = channelTypeUID.getId().split("-")[0];
try {
Object result = measureDef.limit.isBlank()
? api.getMeasures(deviceId, moduleId, measureDef.period, descriptor)
: api.getMeasures(deviceId, moduleId, measureDef.period, descriptor,
measureDef.limit);
MeasureClass.AS_SET.stream().filter(mc -> mc.apiDescriptor.equals(descriptor)).findFirst()
.ifPresent(mc -> {
State state = result instanceof ZonedDateTime
? toDateTimeType((ZonedDateTime) result)
: result instanceof Double ? toQuantityType((Double) result, mc)
: UnDefType.UNDEF;
measures.put(channel.getUID().getIdWithoutGroup(), state);
});
} catch (NetatmoException e) {
logger.warn("Error getting measures for channel {}, check configuration",
channel.getLabel());
}
}
});
}
}

View File

@ -0,0 +1,100 @@
/**
* 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.capability;
import static org.openhab.binding.netatmo.internal.NetatmoBindingConstants.*;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.netatmo.internal.api.data.EventType;
import org.openhab.binding.netatmo.internal.api.data.ModuleType;
import org.openhab.binding.netatmo.internal.api.dto.Event;
import org.openhab.binding.netatmo.internal.api.dto.HomeDataModule;
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;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.StateOption;
/**
* {@link PersonCapability} gives the ability to handle Person specifics
*
* @author Gaël L'hopital - Initial contribution
*
*/
@NonNullByDefault
public class PersonCapability extends HomeSecurityThingCapability {
private final ChannelUID cameraChannelUID;
private @Nullable ZonedDateTime lastEventTime;
public PersonCapability(CommonInterface handler, NetatmoDescriptionProvider descriptionProvider,
List<ChannelHelper> channelHelpers) {
super(handler, descriptionProvider, channelHelpers);
this.cameraChannelUID = new ChannelUID(thing.getUID(), GROUP_PERSON_EVENT, CHANNEL_EVENT_CAMERA_ID);
}
@Override
protected void beforeNewData() {
super.beforeNewData();
homeCapability.ifPresent(cap -> {
Stream<HomeDataModule> cameras = cap.getModules().values().stream()
.filter(module -> module.getType() == ModuleType.WELCOME);
descriptionProvider.setStateOptions(cameraChannelUID,
cameras.map(p -> new StateOption(p.getId(), p.getName())).collect(Collectors.toList()));
});
}
@Override
public void handleCommand(String channelName, Command command) {
if ((command instanceof OnOffType) && CHANNEL_PERSON_AT_HOME.equals(channelName)) {
securityCapability.ifPresent(cap -> cap.setPersonAway(handler.getId(), OnOffType.OFF.equals(command)));
}
}
@Override
public void updateEvent(Event event) {
super.updateEvent(event);
EventType eventType = event.getEventType();
ZonedDateTime localLast = lastEventTime;
ZonedDateTime eventTime = event.getTime();
if ((localLast != null && !eventTime.isAfter(localLast)) || !eventType.appliesOn(ModuleType.PERSON)) {
return; // ignore incoming events if they are deprecated
}
lastEventTime = eventTime;
handler.triggerChannel(CHANNEL_HOME_EVENT,
event.getSubTypeDescription().map(st -> st.name()).orElse(event.getEventType().name()));
}
@Override
public List<NAObject> updateReadings() {
List<NAObject> result = new ArrayList<>();
securityCapability.ifPresent(cap -> {
Collection<HomeEvent> events = cap.getPersonEvents(handler.getId());
if (!events.isEmpty()) {
result.add(events.iterator().next());
}
});
return result;
}
}

View File

@ -0,0 +1,67 @@
/**
* 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.capability;
import static org.openhab.binding.netatmo.internal.NetatmoBindingConstants.CHANNEL_FLOODLIGHT;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.netatmo.internal.api.data.NetatmoConstants.FloodLightMode;
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;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* {@link PresenceCapability} give to handle Presence Camera specifics
*
* @author Gaël L'hopital - Initial contribution
*
*/
@NonNullByDefault
public class PresenceCapability extends CameraCapability {
private final Logger logger = LoggerFactory.getLogger(PresenceCapability.class);
public PresenceCapability(CommonInterface handler, NetatmoDescriptionProvider descriptionProvider,
List<ChannelHelper> channelHelpers) {
super(handler, descriptionProvider, channelHelpers);
}
@Override
public void handleCommand(String channelName, Command command) {
if (CHANNEL_FLOODLIGHT.equals(channelName)) {
if (command instanceof OnOffType) {
changeFloodlightMode(command == OnOffType.ON ? FloodLightMode.ON : FloodLightMode.OFF);
return;
} else if (command instanceof StringType) {
try {
FloodLightMode mode = FloodLightMode.valueOf(command.toString());
changeFloodlightMode(mode);
} catch (IllegalArgumentException e) {
logger.info("Incorrect command '{}' received for channel '{}'", command, channelName);
}
return;
}
}
super.handleCommand(channelName, command);
}
private void changeFloodlightMode(FloodLightMode mode) {
securityCapability.ifPresent(cap -> cap.changeFloodlightMode(localUrl, mode));
}
}

View File

@ -0,0 +1,124 @@
/**
* 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.capability;
import static java.time.temporal.ChronoUnit.*;
import java.time.Duration;
import java.time.Instant;
import java.util.Optional;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.netatmo.internal.api.dto.NAThing;
import org.openhab.binding.netatmo.internal.handler.CommonInterface;
import org.openhab.core.thing.ThingStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* {@link RefreshCapability} is the class used to embed the refreshing needs calculation for devices
*
* @author Gaël L'hopital - Initial contribution
*
*/
@NonNullByDefault
public class RefreshCapability extends Capability {
private static final Duration DEFAULT_DELAY = Duration.of(20, SECONDS);
private static final Duration PROBING_INTERVAL = Duration.of(120, SECONDS);
private static final Duration OFFLINE_INTERVAL = Duration.of(15, MINUTES);
private final Logger logger = LoggerFactory.getLogger(RefreshCapability.class);
private final ScheduledExecutorService scheduler;
private Duration dataValidity;
private Instant dataTimeStamp = Instant.now();
private Instant dataTimeStamp0 = Instant.MIN;
private Optional<ScheduledFuture<?>> refreshJob = Optional.empty();
private final boolean refreshConfigured;
public RefreshCapability(CommonInterface handler, ScheduledExecutorService scheduler, int refreshInterval) {
super(handler);
this.scheduler = scheduler;
this.dataValidity = Duration.ofSeconds(Math.max(0, refreshInterval));
this.refreshConfigured = !probing();
freeJobAndReschedule(2);
}
@Override
public void dispose() {
freeJobAndReschedule(0);
super.dispose();
}
@Override
public void expireData() {
dataTimeStamp = Instant.now().minus(dataValidity);
freeJobAndReschedule(1);
}
private Duration dataAge() {
return Duration.between(dataTimeStamp, Instant.now());
}
private boolean probing() {
return dataValidity.getSeconds() <= 0;
}
private void proceedWithUpdate() {
handler.proceedWithUpdate();
long delay;
if (!ThingStatus.ONLINE.equals(handler.getThing().getStatus())) {
logger.debug("Module is not ONLINE; special refresh interval is used");
delay = OFFLINE_INTERVAL.toSeconds();
if (probing()) {
dataTimeStamp0 = Instant.MIN;
}
} else if (refreshConfigured) {
delay = dataValidity.getSeconds();
} else {
delay = (probing() ? PROBING_INTERVAL : dataValidity.minus(dataAge()).plus(DEFAULT_DELAY)).toSeconds();
}
delay = delay < 2 ? PROBING_INTERVAL.toSeconds() : delay;
logger.debug("Module refreshed, next one in {} s", delay);
freeJobAndReschedule(delay);
}
@Override
protected void updateNAThing(NAThing newData) {
super.updateNAThing(newData);
newData.getLastSeen().ifPresent(timestamp -> {
Instant tsInstant = timestamp.toInstant();
if (probing()) {
if (Instant.MIN.equals(dataTimeStamp0)) {
dataTimeStamp0 = tsInstant;
logger.debug("First data timestamp is {}", dataTimeStamp0);
} else if (tsInstant.isAfter(dataTimeStamp0)) {
dataValidity = Duration.between(dataTimeStamp0, tsInstant);
logger.debug("Data validity period identified to be {}", dataValidity);
} else {
logger.debug("Data validity period not yet found - data timestamp unchanged");
}
}
dataTimeStamp = tsInstant;
});
}
private void freeJobAndReschedule(long delay) {
refreshJob.ifPresent(job -> job.cancel(false));
refreshJob = delay == 0 ? Optional.empty()
: Optional.of(scheduler.schedule(() -> proceedWithUpdate(), delay, TimeUnit.SECONDS));
}
}

View File

@ -0,0 +1,76 @@
/**
* 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.capability;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.netatmo.internal.api.RestManager;
import org.openhab.binding.netatmo.internal.api.dto.Device;
import org.openhab.binding.netatmo.internal.api.dto.Module;
import org.openhab.binding.netatmo.internal.api.dto.NAObject;
import org.openhab.binding.netatmo.internal.deserialization.NAObjectMap;
import org.openhab.binding.netatmo.internal.handler.ApiBridgeHandler;
import org.openhab.binding.netatmo.internal.handler.CommonInterface;
/**
* The {@link RestCapability} is the base class for handler capabilities
*
* @author Gaël L'hopital - Initial contribution
*
*/
@NonNullByDefault
public abstract class RestCapability<T extends RestManager> extends DeviceCapability {
private Optional<T> api = Optional.empty();
private Class<T> restManagerClass;
RestCapability(CommonInterface handler, Class<T> restManagerClazz) {
super(handler);
this.restManagerClass = restManagerClazz;
}
@Override
protected void updateNADevice(Device newData) {
super.updateNADevice(newData);
NAObjectMap<Module> modules = newData.getModules();
handler.getActiveChildren().forEach(child -> {
Module childData = modules.get(child.getId());
if (childData != null) {
child.setNewData(childData);
}
});
}
@Override
public final List<NAObject> updateReadings() {
List<NAObject> result = new ArrayList<>();
getApi().ifPresent(api -> result.addAll(updateReadings(api)));
return result;
}
protected List<NAObject> updateReadings(T api) {
return List.of();
}
protected Optional<T> getApi() {
if (api.isEmpty()) {
ApiBridgeHandler bridgeApi = handler.getAccountHandler();
if (bridgeApi != null) {
api = Optional.ofNullable(bridgeApi.getRestManager(restManagerClass));
}
}
return api;
}
}

View File

@ -0,0 +1,80 @@
/**
* 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.capability;
import static org.openhab.binding.netatmo.internal.NetatmoBindingConstants.*;
import static org.openhab.binding.netatmo.internal.utils.ChannelTypeUtils.commandToQuantity;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.netatmo.internal.action.RoomActions;
import org.openhab.binding.netatmo.internal.api.data.NetatmoConstants.MeasureClass;
import org.openhab.binding.netatmo.internal.api.data.NetatmoConstants.SetpointMode;
import org.openhab.binding.netatmo.internal.handler.CommonInterface;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* {@link RoomCapability} gives the ability to handle Room specifics
*
* @author Gaël L'hopital - Initial contribution
*
*/
@NonNullByDefault
public class RoomCapability extends Capability {
private final Logger logger = LoggerFactory.getLogger(RoomCapability.class);
private Optional<EnergyCapability> energyCapability = Optional.empty();
public RoomCapability(CommonInterface handler) {
super(handler);
}
@Override
public void initialize() {
energyCapability = handler.getHomeCapability(EnergyCapability.class);
}
@Override
public void handleCommand(String channelName, Command command) {
if (CHANNEL_SETPOINT_MODE.equals(channelName)) {
try {
SetpointMode targetMode = SetpointMode.valueOf(command.toString());
if (targetMode == SetpointMode.MANUAL) {
logger.info("Switch to 'Manual' mode is done by setting a setpoint temp, command ignored");
} else {
energyCapability.ifPresent(cap -> cap.setRoomThermMode(handler.getId(), targetMode));
}
} catch (IllegalArgumentException e) {
logger.info("Command '{}' is not a valid setpoint mode for channel '{}'", command, channelName);
}
} else if (CHANNEL_VALUE.equals(channelName)) {
QuantityType<?> quantity = commandToQuantity(command, MeasureClass.INSIDE_TEMPERATURE);
if (quantity != null) {
energyCapability.ifPresent(cap -> cap.setRoomThermTemp(handler.getId(), quantity.doubleValue()));
} else {
logger.warn("Incorrect command '{}' on channel '{}'", command, channelName);
}
}
}
@Override
public Collection<Class<? extends ThingHandlerService>> getServices() {
return List.of(RoomActions.class);
}
}

View File

@ -0,0 +1,175 @@
/**
* 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.capability;
import java.util.Collection;
import java.util.List;
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.SecurityApi;
import org.openhab.binding.netatmo.internal.api.data.NetatmoConstants.FloodLightMode;
import org.openhab.binding.netatmo.internal.api.dto.HomeData;
import org.openhab.binding.netatmo.internal.api.dto.HomeDataModule;
import org.openhab.binding.netatmo.internal.api.dto.HomeDataPerson;
import org.openhab.binding.netatmo.internal.api.dto.HomeEvent;
import org.openhab.binding.netatmo.internal.api.dto.HomeStatusModule;
import org.openhab.binding.netatmo.internal.api.dto.HomeStatusPerson;
import org.openhab.binding.netatmo.internal.api.dto.NAHomeStatus.HomeStatus;
import org.openhab.binding.netatmo.internal.deserialization.NAObjectMap;
import org.openhab.binding.netatmo.internal.handler.CommonInterface;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link SecurityCapability} is the base class for handler able to handle security features
*
* @author Gaël L'hopital - Initial contribution
*
*/
@NonNullByDefault
class SecurityCapability extends RestCapability<SecurityApi> {
private final Logger logger = LoggerFactory.getLogger(SecurityCapability.class);
SecurityCapability(CommonInterface handler) {
super(handler, SecurityApi.class);
}
@Override
protected void updateHomeData(HomeData homeData) {
NAObjectMap<HomeDataPerson> persons = homeData.getPersons();
NAObjectMap<HomeDataModule> modules = homeData.getModules();
handler.getActiveChildren().forEach(childHandler -> {
String childId = childHandler.getId();
persons.getOpt(childId).ifPresentOrElse(person -> {
person.setIgnoredForThingUpdate(true);
childHandler.setNewData(person);
}, () -> {
modules.getOpt(childId).ifPresent(module -> {
module.setIgnoredForThingUpdate(true);
childHandler.setNewData(module);
});
});
});
}
@Override
protected void updateHomeStatus(HomeStatus homeStatus) {
NAObjectMap<HomeStatusPerson> persons = homeStatus.getPersons();
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), () -> {
// 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);
});
});
});
}
@Override
protected void updateHomeEvent(HomeEvent homeEvent) {
String personId = homeEvent.getPersonId();
if (personId != null) {
handler.getActiveChildren().stream().filter(handler -> personId.equals(handler.getId())).findFirst()
.ifPresent(handler -> {
homeEvent.setIgnoredForThingUpdate(true);
handler.setNewData(homeEvent);
});
}
String cameraId = homeEvent.getCameraId();
handler.getActiveChildren().stream().filter(handler -> cameraId.equals(handler.getId())).findFirst()
.ifPresent(handler -> {
homeEvent.setIgnoredForThingUpdate(true);
handler.setNewData(homeEvent);
});
}
public Collection<HomeEvent> getCameraEvents(String cameraId) {
return getApi().map(api -> {
try {
return api.getCameraEvents(handler.getId(), cameraId);
} catch (NetatmoException e) {
logger.warn("Error retrieving last events of camera '{}' : {}", cameraId, e.getMessage());
return null;
}
}).orElse(List.of());
}
public Collection<HomeEvent> getPersonEvents(String personId) {
return getApi().map(api -> {
try {
return api.getPersonEvents(handler.getId(), personId);
} catch (NetatmoException e) {
logger.warn("Error retrieving last events of person '{}' : {}", personId, e.getMessage());
return null;
}
}).orElse(List.of());
}
public void setPersonAway(String personId, boolean away) {
getApi().ifPresent(api -> {
try {
api.setPersonAwayStatus(handler.getId(), personId, away);
handler.expireData();
} catch (NetatmoException e) {
logger.warn("Error setting person away/at home '{}' : {}", personId, e.getMessage());
}
});
}
public @Nullable String ping(String vpnUrl) {
return getApi().map(api -> {
try {
return api.ping(vpnUrl);
} catch (NetatmoException e) {
logger.warn("Error pinging camera '{}' : {}", vpnUrl, e.getMessage());
return null;
}
}).orElse(null);
}
public void changeStatus(@Nullable String localURL, boolean status) {
if (localURL == null) {
logger.info("Monitoring changes can only be done on local camera.");
return;
}
getApi().ifPresent(api -> {
try {
api.changeStatus(localURL, status);
handler.expireData();
} catch (NetatmoException e) {
logger.warn("Error changing camera monitoring status '{}' : {}", status, e.getMessage());
}
});
}
public void changeFloodlightMode(@Nullable String localURL, FloodLightMode mode) {
if (localURL == null) {
logger.info("Changing floodlight mode can only be done on local camera.");
return;
}
getApi().ifPresent(api -> {
try {
api.changeFloodLightMode(localURL, mode);
handler.expireData();
} catch (NetatmoException e) {
logger.warn("Error changing Presence floodlight mode '{}' : {}", mode, e.getMessage());
}
});
}
}

View File

@ -0,0 +1,48 @@
/**
* 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.capability;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.netatmo.internal.api.NetatmoException;
import org.openhab.binding.netatmo.internal.api.WeatherApi;
import org.openhab.binding.netatmo.internal.api.dto.NAObject;
import org.openhab.binding.netatmo.internal.handler.CommonInterface;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* {@link WeatherCapability} give the ability to read weather station api
*
* @author Gaël L'hopital - Initial contribution
*
*/
@NonNullByDefault
public class WeatherCapability extends RestCapability<WeatherApi> {
private final Logger logger = LoggerFactory.getLogger(WeatherCapability.class);
public WeatherCapability(CommonInterface handler) {
super(handler, WeatherApi.class);
}
@Override
protected List<NAObject> updateReadings(WeatherApi api) {
try {
return List.of(api.getStationData(handler.getId()));
} catch (NetatmoException e) {
logger.warn("Error retrieving weather data '{}' : {}", handler.getId(), e.getMessage());
}
return List.of();
}
}

Some files were not shown because too many files have changed in this diff Show More