added migrated 2.x add-ons

Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
Kai Kreuzer
2020-09-21 01:58:32 +02:00
parent bbf1a7fd29
commit 6df6783b60
11662 changed files with 1302875 additions and 11 deletions

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.netatmo-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
<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 dependency="true">mvn:org.openhab.osgiify/com.squareup.okhttp.okhttp/2.3.0</bundle>
<bundle dependency="true">mvn:org.openhab.osgiify/com.squareup.okio.okio-1.3.0/1.3.0</bundle>
<bundle dependency="true">mvn:org.openhab.osgiify/com.squareup.retrofit.retrofit/1.9.0</bundle>
<bundle dependency="true">mvn:org.apache.oltu.oauth2/org.apache.oltu.oauth2.client/1.0.0</bundle>
<bundle dependency="true">mvn:org.apache.oltu.oauth2/org.apache.oltu.oauth2.common/1.0.0</bundle>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.netatmo/${project.version}</bundle>
</feature>
</features>

View File

@@ -0,0 +1,137 @@
/**
* Copyright (c) 2010-2020 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.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.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;
import org.openhab.core.io.net.http.HttpUtil;
/**
* 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, BigDecimal.ROUND_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

@@ -0,0 +1,42 @@
/**
* Copyright (c) 2010-2020 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 org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.binding.BaseDynamicStateDescriptionProvider;
import org.openhab.core.thing.i18n.ChannelTypeI18nLocalizationService;
import org.openhab.core.thing.type.DynamicStateDescriptionProvider;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* Dynamic provider of state options for NATherm1Handler.
*
* @author Gregory Moyer - Initial contribution
* @author Gaël L'hopital - Ported as-is in Netatmo binding
*/
@Component(service = { DynamicStateDescriptionProvider.class, NATherm1StateDescriptionProvider.class })
@NonNullByDefault
public class NATherm1StateDescriptionProvider extends BaseDynamicStateDescriptionProvider {
@Reference
protected void setChannelTypeI18nLocalizationService(
final ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) {
this.channelTypeI18nLocalizationService = channelTypeI18nLocalizationService;
}
protected void unsetChannelTypeI18nLocalizationService(
final ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) {
this.channelTypeI18nLocalizationService = null;
}
}

View File

@@ -0,0 +1,305 @@
/**
* Copyright (c) 2010-2020 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.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.binding.netatmo.internal.webhook.NAWebhookCameraEvent.EventTypeEnum;
/**
* The {@link NetatmoBinding} 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 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";
// 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";
// 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";
// 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<String> MEASURABLE_CHANNELS = Stream.of(new String[] {}).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());
}

View File

@@ -0,0 +1,198 @@
/**
* Copyright (c) 2010-2020 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 static org.openhab.binding.netatmo.internal.NetatmoBindingConstants.*;
import java.util.Dictionary;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
import javax.servlet.http.HttpServlet;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
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.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.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
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.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.Reference;
import org.osgi.service.http.HttpService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link NetatmoHandlerFactory} is responsible for creating things and
* thing handlers.
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.netatmo")
public class NetatmoHandlerFactory extends BaseThingHandlerFactory {
private final Logger logger = LoggerFactory.getLogger(NetatmoHandlerFactory.class);
private final Map<ThingUID, @Nullable ServiceRegistration<?>> discoveryServiceRegs = new HashMap<>();
private final Map<ThingUID, @Nullable ServiceRegistration<?>> webHookServiceRegs = new HashMap<>();
private final HttpService httpService;
private final NATherm1StateDescriptionProvider stateDescriptionProvider;
private final TimeZoneProvider timeZoneProvider;
private final LocaleProvider localeProvider;
private final TranslationProvider translationProvider;
private boolean backgroundDiscovery;
@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;
this.stateDescriptionProvider = stateDescriptionProvider;
this.timeZoneProvider = timeZoneProvider;
this.localeProvider = localeProvider;
this.translationProvider = translationProvider;
}
@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;
}
logger.debug("backgroundDiscovery {}", backgroundDiscovery);
}
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return (SUPPORTED_THING_TYPES_UIDS.contains(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;
}
}
@Override
protected void removeHandler(ThingHandler thingHandler) {
if (thingHandler instanceof NetatmoBridgeHandler) {
ThingUID thingUID = thingHandler.getThing().getUID();
unregisterDeviceDiscoveryService(thingUID);
unregisterWebHookServlet(thingUID);
}
}
private synchronized void registerDeviceDiscoveryService(NetatmoBridgeHandler netatmoBridgeHandler) {
if (bundleContext != null) {
NetatmoModuleDiscoveryService discoveryService = new NetatmoModuleDiscoveryService(netatmoBridgeHandler,
localeProvider, translationProvider);
Map<String, @Nullable 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();
}
}
}
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;
}
private synchronized void unregisterWebHookServlet(ThingUID thingUID) {
ServiceRegistration<?> serviceReg = webHookServiceRegs.remove(thingUID);
if (serviceReg != null) {
serviceReg.unregister();
}
}
}

View File

@@ -0,0 +1,99 @@
/**
* Copyright (c) 2010-2020 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,86 @@
/**
* Copyright (c) 2010-2020 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 org.eclipse.jdt.annotation.NonNullByDefault;
/**
* This class holds various unit/measurement conversion methods
*
* @author Gaël L'hopital - Initial contribution
* @author Rob Nielsen - updated heat index
*/
@NonNullByDefault
public class WeatherUtils {
/**
* Calculate the heat index using temperature and humidity
* https://www.wpc.ncep.noaa.gov/html/heatindex_equation.shtml
*
* @param temperature in (°C)
* @param humidity relative level (%)
* @return heatIndex in (°C)
*/
public static double getHeatIndex(double temperature, double humidity) {
double tempF = (temperature * 9.0 / 5.0) + 32.0; // calculations are done in Fahrenheit
double heatIndex;
if (tempF >= 80.0) {
heatIndex = -42.379 + (2.04901523 * tempF) + (10.14333127 * humidity) - (0.22475541 * tempF * humidity)
- (0.00683783 * tempF * tempF) - (0.05481717 * humidity * humidity)
+ (0.00122874 * tempF * tempF * humidity) + (0.00085282 * tempF * humidity * humidity)
- (0.00000199 * tempF * tempF * humidity * humidity);
if (humidity < 13.0 && tempF <= 112.0) {
heatIndex -= ((13.0 - humidity) / 4.0) * Math.sqrt((17.0 - Math.abs(tempF - 95.0)) / 17.0);
} else if (humidity > 85.0 && tempF <= 87.0) {
heatIndex += ((humidity - 85.0) / 10.0) * ((87.0 - tempF) / 5.0);
}
} else {
heatIndex = 0.5 * (tempF + 61.0 + ((tempF - 68.0) * 1.2) + (humidity * 0.094));
}
return (heatIndex - 32) * 5.0 / 9.0; // convert back to Celsius
}
public static double getDewPointDep(double temperature, double dewpoint) {
return temperature - dewpoint;
}
/**
* Compute the Dewpoint temperature given temperature and hygrometry
* valid up to 60 degrees, from
* http://en.wikipedia.org/wiki/Dew_point#Calculating_the_dew_point
*
* @param temperature in (°C)
* @param humidity relative level (%)
* @return dewpoint temperature
*/
public static double getDewPoint(double temperature, double humidity) {
double a = 17.271, b = 237.2;
double gamma = ((a * temperature) / (b + temperature)) + Math.log(humidity / 100.0);
return b * gamma / (a - gamma);
}
/**
* Compute the Humidex index given temperature and hygrometry
*
*
* @param temperature in (°C)
* @param hygro relative level (%)
* @return Humidex index value
*/
public static double getHumidex(double temperature, double hygro) {
double result = 6.112 * Math.pow(10, 7.5 * temperature / (237.7 + temperature)) * hygro / 100;
result = temperature + 0.555555556 * (result - 10);
return result;
}
}

View File

@@ -0,0 +1,70 @@
/**
* Copyright (c) 2010-2020 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

@@ -0,0 +1,246 @@
/**
* Copyright (c) 2010-2020 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.eclipse.jdt.annotation.Nullable;
import org.openhab.core.i18n.TimeZoneProvider;
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.openhab.core.io.net.http.HttpUtil;
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.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 @Nullable CameraAddress cameraAddress;
protected CameraHandler(Thing thing, final TimeZoneProvider timeZoneProvider) {
super(thing, timeZoneProvider);
}
@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.getIsLocal())).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::getIsLocal).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();
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 == null || address.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)).orElse(null);
cameraAddress = address;
}
}
return Optional.ofNullable(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

@@ -0,0 +1,80 @@
/**
* Copyright (c) 2010-2020 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.core.library.types.OnOffType;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import org.openhab.binding.netatmo.internal.ChannelTypeUtils;
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

@@ -0,0 +1,85 @@
/**
* Copyright (c) 2010-2020 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,37 @@
/**
* Copyright (c) 2010-2020 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,245 @@
/**
* Copyright (c) 2010-2020 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.*;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
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.openhab.binding.netatmo.internal.handler.NetatmoBridgeHandler;
import org.openhab.binding.netatmo.internal.handler.NetatmoDataListener;
import org.osgi.framework.Bundle;
import org.osgi.framework.FrameworkUtil;
import io.swagger.client.model.*;
/**
* 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, @Nullable 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 -> {
dataBody.getDevices().forEach(station -> {
discoverWeatherStation(station);
});
});
}
if (netatmoBridgeHandler.configuration.readHealthyHomeCoach) {
netatmoBridgeHandler.getHomecoachDataBody(null).ifPresent(dataBody -> {
dataBody.getDevices().forEach(homecoach -> {
discoverHomeCoach(homecoach);
});
});
}
if (netatmoBridgeHandler.configuration.readThermostat) {
netatmoBridgeHandler.getThermostatsDataBody(null).ifPresent(dataBody -> {
dataBody.getDevices().forEach(plug -> {
discoverThermostat(plug);
});
});
}
if (netatmoBridgeHandler.configuration.readWelcome || netatmoBridgeHandler.configuration.readPresence) {
netatmoBridgeHandler.getWelcomeDataBody(null).ifPresent(dataBody -> {
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());
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.getFavorite() != null && station.getFavorite();
final String weatherStationName = createWeatherStationName(station, isFavorite);
onDeviceAddedInternal(station.getId(), null, station.getType(), weatherStationName, station.getFirmware());
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
if (!home.getCameras().isEmpty()) {
onDeviceAddedInternal(home.getId(), null, WELCOME_HOME_THING_TYPE.getId(), home.getName(), null);
// Discover Cameras
home.getCameras().forEach(camera -> {
onDeviceAddedInternal(camera.getId(), home.getId(), camera.getType(), camera.getName(), null);
});
// Discover Known Persons
home.getPersons().stream().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

@@ -0,0 +1,289 @@
/**
* Copyright (c) 2010-2020 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.core.library.unit.MetricPrefix.*;
import static org.openhab.binding.netatmo.internal.NetatmoBindingConstants.*;
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.core.config.core.Configuration;
import org.openhab.core.i18n.TimeZoneProvider;
import org.openhab.core.library.unit.SIUnits;
import org.openhab.core.library.unit.SmartHomeUnits;
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.openhab.binding.netatmo.internal.channelhelper.BatteryHelper;
import org.openhab.binding.netatmo.internal.channelhelper.RadioHelper;
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 = SmartHomeUnits.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 = SmartHomeUnits.DEGREE_ANGLE;
public static final Unit<Length> API_RAIN_UNIT = MILLI(SIUnits.METRE);
public static final Unit<Dimensionless> API_CO2_UNIT = SmartHomeUnits.PARTS_PER_MILLION;
public static final Unit<Dimensionless> API_NOISE_UNIT = SmartHomeUnits.DECIBEL;
private final Logger logger = LoggerFactory.getLogger(AbstractNetatmoThingHandler.class);
protected final TimeZoneProvider timeZoneProvider;
protected final MeasurableChannels measurableChannels = new MeasurableChannels();
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();
radioHelper = thing.getProperties().containsKey(PROPERTY_SIGNAL_LEVELS)
? new RadioHelper(thing.getProperties().get(PROPERTY_SIGNAL_LEVELS))
: null;
batteryHelper = thing.getProperties().containsKey(PROPERTY_BATTERY_LEVELS)
? new BatteryHelper(thing.getProperties().get(PROPERTY_BATTERY_LEVELS))
: 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;
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();
}
result = measurableChannels.getNAThingProperty(channelId);
return result.orElse(UnDefType.UNDEF);
}
protected void updateChannels() {
if (thing.getStatus() != ThingStatus.ONLINE) {
return;
}
updateDataChannels();
triggerEventChannels();
}
private void updateDataChannels() {
getThing().getChannels().stream().filter(channel -> !channel.getKind().equals(ChannelKind.TRIGGER))
.forEach(channel -> {
String channelId = channel.getUID().getId();
if (isLinked(channelId)) {
State state = getNAThingProperty(channelId);
updateState(channel.getUID(), state);
}
});
}
/**
* 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 -> channel.getKind().equals(ChannelKind.TRIGGER))
.forEach(channel -> triggerChannelIfRequired(channel.getUID().getId()));
}
/**
* 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 channelLinked(ChannelUID channelUID) {
super.channelLinked(channelUID);
measurableChannels.addChannel(channelUID);
}
@Override
public void channelUnlinked(ChannelUID channelUID) {
super.channelUnlinked(channelUID);
measurableChannels.removeChannel(channelUID);
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (command == RefreshType.REFRESH) {
logger.debug("Refreshing {}", channelUID);
updateChannels();
}
}
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,91 @@
/**
* Copyright (c) 2010-2020 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.MEASURABLE_CHANNELS;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.types.State;
import org.openhab.binding.netatmo.internal.ChannelTypeUtils;
import io.swagger.client.CollectionFormats.CSVParams;
import io.swagger.client.model.NAMeasureResponse;
/**
* {@link MeasurableChannels} is a helper class designed to handle
* manipulation of requests and responses provided by calls to
* someNetatmoApi.getMeasures(....)
*
* @author Gaël L'hopital - Initial contribution
*
*/
@NonNullByDefault
public class MeasurableChannels {
protected @Nullable NAMeasureResponse measures;
protected List<String> measuredChannels = new ArrayList<>();
/*
* If this channel value is provided as a measure, then add it
* in the getMeasure parameter list
*/
protected void addChannel(ChannelUID channelUID) {
String channel = channelUID.getId();
if (MEASURABLE_CHANNELS.contains(channel)) {
measuredChannels.add(channel);
}
}
/*
* If this channel value is provided as a measure, then delete
* it in the getMeasure parameter list
*/
protected void removeChannel(ChannelUID channelUID) {
String channel = channelUID.getId();
measuredChannels.remove(channel);
}
protected Optional<State> getNAThingProperty(String channelId) {
int index = measuredChannels.indexOf(channelId);
NAMeasureResponse theMeasures = measures;
if (index != -1 && theMeasures != null) {
if (!theMeasures.getBody().isEmpty()) {
List<List<Float>> valueList = theMeasures.getBody().get(0).getValue();
if (!valueList.isEmpty()) {
List<Float> values = valueList.get(0);
if (values.size() >= index) {
Float value = values.get(index);
return Optional.of(ChannelTypeUtils.toDecimalType(value));
}
}
}
}
return Optional.empty();
}
public Optional<CSVParams> getAsCsv() {
if (!measuredChannels.isEmpty()) {
return Optional.of(new CSVParams(measuredChannels));
}
return Optional.empty();
}
public void setMeasures(NAMeasureResponse measures) {
this.measures = measures;
}
}

View File

@@ -0,0 +1,396 @@
/**
* Copyright (c) 2010-2020 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.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
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.request.OAuthClientRequest;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
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.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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.squareup.okhttp.OkHttpClient;
import io.swagger.client.ApiClient;
import io.swagger.client.CollectionFormats.CSVParams;
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.OAuth;
import io.swagger.client.auth.OAuthFlow;
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;
import retrofit.RestAdapter.LogLevel;
import retrofit.RetrofitError;
import retrofit.RetrofitError.Kind;
/**
* {@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 APIMap apiMap;
private @Nullable WelcomeWebHookServlet webHookServlet;
private List<NetatmoDataListener> dataListeners = new CopyOnWriteArrayList<>();
private class APIMap extends HashMap<Class<?>, Object> {
private static final long serialVersionUID = -2024031764691952343L;
private ApiClient apiClient;
public APIMap(ApiClient apiClient) {
super();
this.apiClient = apiClient;
}
public Object get(Class<?> apiClass) {
if (!super.containsKey(apiClass)) {
Object api = apiClient.createService(apiClass);
super.put(apiClass, api);
}
return super.get(apiClass);
}
}
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 (RetrofitError e) {
if (e.getKind() == Kind.NETWORK) {
logger.warn("Network error while connecting to Netatmo API, will retry in {} s",
configuration.reconnectInterval);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"Netatmo Access Failed, will retry in " + configuration.reconnectInterval + " seconds.");
} else {
switch (e.getResponse().getStatus()) {
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());
return;
}
}
} catch (RuntimeException e) {
logger.warn("Unable to connect Netatmo API : {}", e.getMessage(), e);
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() throws RetrofitError {
ApiClient apiClient = new ApiClient();
OAuth auth = new OAuth(new OkHttpClient(),
OAuthClientRequest.tokenLocation("https://api.netatmo.net/oauth2/token"));
auth.setFlow(OAuthFlow.password);
auth.setAuthenticationRequestBuilder(OAuthClientRequest.authorizationLocation(""));
apiClient.getApiAuthorizations().put("password_oauth", auth);
apiClient.getTokenEndPoint().setClientId(configuration.clientId).setClientSecret(configuration.clientSecret)
.setUsername(configuration.username).setPassword(configuration.password).setScope(getApiScope());
apiClient.configureFromOkclient(new OkHttpClient());
apiClient.getAdapterBuilder().setLogLevel(logger.isDebugEnabled() ? LogLevel.FULL : LogLevel.NONE);
apiMap = new APIMap(apiClient);
}
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() {
APIMap map = apiMap;
return map != null ? (PartnerApi) map.get(PartnerApi.class) : null;
}
public Optional<StationApi> getStationApi() {
APIMap map = apiMap;
return map != null ? Optional.of((StationApi) map.get(StationApi.class)) : Optional.empty();
}
public Optional<HealthyhomecoachApi> getHomeCoachApi() {
APIMap map = apiMap;
return map != null ? Optional.of((HealthyhomecoachApi) map.get(HealthyhomecoachApi.class)) : Optional.empty();
}
public Optional<ThermostatApi> getThermostatApi() {
APIMap map = apiMap;
return map != null ? Optional.of((ThermostatApi) map.get(ThermostatApi.class)) : Optional.empty();
}
public Optional<WelcomeApi> getWelcomeApi() {
APIMap map = apiMap;
return map != null ? Optional.of((WelcomeApi) map.get(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, new CSVParams(types), moduleId, null, "last", 1, true, false).getBody())
.orElse(null);
updateStatus(ThingStatus.ONLINE);
NAMeasureBodyElem element = (data != null && data.size() > 0) ? 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

@@ -0,0 +1,32 @@
/**
* Copyright (c) 2010-2020 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

@@ -0,0 +1,257 @@
/**
* Copyright (c) 2010-2020 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.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.openhab.binding.netatmo.internal.ChannelTypeUtils;
import org.openhab.binding.netatmo.internal.RefreshStrategy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.swagger.client.model.NAPlace;
import retrofit.RetrofitError;
/**
* {@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 (RetrofitError 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

@@ -0,0 +1,145 @@
/**
* Copyright (c) 2010-2020 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.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.openhab.binding.netatmo.internal.ChannelTypeUtils;
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,117 @@
/**
* Copyright (c) 2010-2020 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.homecoach;
import static org.openhab.binding.netatmo.internal.ChannelTypeUtils.*;
import static org.openhab.binding.netatmo.internal.NetatmoBindingConstants.*;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.i18n.TimeZoneProvider;
import org.openhab.core.thing.Thing;
import org.openhab.core.types.State;
import org.openhab.binding.netatmo.internal.handler.NetatmoDeviceHandler;
import io.swagger.client.model.NADashboardData;
import io.swagger.client.model.NAHealthyHomeCoach;
/**
* {@link NAHealthyHomeCoachHandler} is the class used to handle the Health Home Coach device
*
* @author Michael Svinth - Initial contribution OH2 version
*
*/
@NonNullByDefault
public class NAHealthyHomeCoachHandler extends NetatmoDeviceHandler<NAHealthyHomeCoach> {
public NAHealthyHomeCoachHandler(Thing thing, final TimeZoneProvider timeZoneProvider) {
super(thing, timeZoneProvider);
}
@Override
protected Optional<NAHealthyHomeCoach> updateReadings() {
return getBridgeHandler().flatMap(handler -> handler.getHomecoachDataBody(getId()))
.map(dataBody -> dataBody.getDevices().stream()
.filter(device -> device.getId().equalsIgnoreCase(getId())).findFirst().orElse(null));
}
@Override
protected void updateProperties(NAHealthyHomeCoach deviceData) {
updateProperties(deviceData.getFirmware(), deviceData.getType());
}
@Override
protected State getNAThingProperty(String channelId) {
NADashboardData dashboardData = getDevice().map(d -> d.getDashboardData()).orElse(null);
if (dashboardData != null) {
switch (channelId) {
case CHANNEL_CO2:
return toQuantityType(dashboardData.getCO2(), API_CO2_UNIT);
case CHANNEL_TEMPERATURE:
return toQuantityType(dashboardData.getTemperature(), API_TEMPERATURE_UNIT);
case CHANNEL_HEALTH_INDEX:
return toStringType(toHealthIndexString(dashboardData.getHealthIdx()));
case CHANNEL_MIN_TEMP:
return toQuantityType(dashboardData.getMinTemp(), API_TEMPERATURE_UNIT);
case CHANNEL_MAX_TEMP:
return toQuantityType(dashboardData.getMaxTemp(), API_TEMPERATURE_UNIT);
case CHANNEL_TEMP_TREND:
return toStringType(dashboardData.getTempTrend());
case CHANNEL_NOISE:
return toQuantityType(dashboardData.getNoise(), API_NOISE_UNIT);
case CHANNEL_PRESSURE:
return toQuantityType(dashboardData.getPressure(), API_PRESSURE_UNIT);
case CHANNEL_PRESS_TREND:
return toStringType(dashboardData.getPressureTrend());
case CHANNEL_ABSOLUTE_PRESSURE:
return toQuantityType(dashboardData.getAbsolutePressure(), API_PRESSURE_UNIT);
case CHANNEL_TIMEUTC:
return toDateTimeType(dashboardData.getTimeUtc(), timeZoneProvider.getTimeZone());
case CHANNEL_DATE_MIN_TEMP:
return toDateTimeType(dashboardData.getDateMinTemp(), timeZoneProvider.getTimeZone());
case CHANNEL_DATE_MAX_TEMP:
return toDateTimeType(dashboardData.getDateMaxTemp(), timeZoneProvider.getTimeZone());
case CHANNEL_HUMIDITY:
return toQuantityType(dashboardData.getHumidity(), API_HUMIDITY_UNIT);
}
}
return super.getNAThingProperty(channelId);
}
private @Nullable String toHealthIndexString(@Nullable Integer healthIndex) {
if (healthIndex == null) {
return null;
}
switch (healthIndex) {
case 0:
return "healthy";
case 1:
return "fine";
case 2:
return "fair";
case 3:
return "poor";
case 4:
return "unhealthy";
default:
return healthIndex.toString();
}
}
@Override
protected Optional<Integer> getDataTimestamp() {
return getDevice().map(d -> d.getLastStatusStore());
}
}

View File

@@ -0,0 +1,125 @@
/**
* Copyright (c) 2010-2020 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.presence;
import static org.openhab.binding.netatmo.internal.ChannelTypeUtils.toOnOffType;
import static org.openhab.binding.netatmo.internal.NetatmoBindingConstants.*;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.i18n.TimeZoneProvider;
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.openhab.binding.netatmo.internal.camera.CameraHandler;
import io.swagger.client.model.NAWelcomeCamera;
/**
* {@link NAPresenceCameraHandler} is the class used to handle Presence camera data
*
* @author Sven Strohschein - Initial contribution
*/
@NonNullByDefault
public class NAPresenceCameraHandler extends CameraHandler {
private static final String FLOODLIGHT_SET_URL_PATH = "/command/floodlight_set_config";
private State floodlightAutoModeState = UnDefType.UNDEF;
public NAPresenceCameraHandler(final Thing thing, final TimeZoneProvider timeZoneProvider) {
super(thing, timeZoneProvider);
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
String channelId = channelUID.getId();
switch (channelId) {
case CHANNEL_CAMERA_FLOODLIGHT:
if (command == OnOffType.ON) {
switchFloodlight(true);
} else if (command == OnOffType.OFF) {
switchFloodlight(false);
}
break;
case CHANNEL_CAMERA_FLOODLIGHT_AUTO_MODE:
if (command == OnOffType.ON) {
switchFloodlightAutoMode(true);
} else if (command == OnOffType.OFF) {
switchFloodlightAutoMode(false);
}
break;
}
super.handleCommand(channelUID, command);
}
@Override
protected State getNAThingProperty(String channelId) {
switch (channelId) {
case CHANNEL_CAMERA_FLOODLIGHT:
return getFloodlightState();
case CHANNEL_CAMERA_FLOODLIGHT_AUTO_MODE:
// The auto-mode state shouldn't be updated, because this isn't a dedicated information. When the
// floodlight is switched on the state within the Netatmo API is "on" and the information if the
// previous
// state was "auto" instead of "off" is lost... Therefore the binding handles its own auto-mode state.
if (floodlightAutoModeState == UnDefType.UNDEF) {
floodlightAutoModeState = getFloodlightAutoModeState();
}
return floodlightAutoModeState;
}
return super.getNAThingProperty(channelId);
}
private State getFloodlightState() {
return getModule().map(m -> toOnOffType(m.getLightModeStatus() == NAWelcomeCamera.LightModeStatusEnum.ON))
.orElse(UnDefType.UNDEF);
}
private State getFloodlightAutoModeState() {
return getModule().map(m -> toOnOffType(m.getLightModeStatus() == NAWelcomeCamera.LightModeStatusEnum.AUTO))
.orElse(UnDefType.UNDEF);
}
private void switchFloodlight(boolean isOn) {
if (isOn) {
changeFloodlightMode(NAWelcomeCamera.LightModeStatusEnum.ON);
} else {
switchFloodlightAutoMode(floodlightAutoModeState == OnOffType.ON);
}
}
private void switchFloodlightAutoMode(boolean isAutoMode) {
floodlightAutoModeState = toOnOffType(isAutoMode);
if (isAutoMode) {
changeFloodlightMode(NAWelcomeCamera.LightModeStatusEnum.AUTO);
} else {
changeFloodlightMode(NAWelcomeCamera.LightModeStatusEnum.OFF);
}
}
private void changeFloodlightMode(NAWelcomeCamera.LightModeStatusEnum mode) {
Optional<String> localCameraURL = getLocalCameraURL();
if (localCameraURL.isPresent()) {
String url = localCameraURL.get() + FLOODLIGHT_SET_URL_PATH + "?config=%7B%22mode%22:%22" + mode.toString()
+ "%22%7D";
executeGETRequest(url);
invalidateParentCacheAndRefresh();
}
}
}

View File

@@ -0,0 +1,296 @@
/**
* Copyright (c) 2010-2020 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.station;
import static org.openhab.binding.netatmo.internal.ChannelTypeUtils.*;
import static org.openhab.binding.netatmo.internal.NetatmoBindingConstants.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.i18n.TimeZoneProvider;
import org.openhab.core.thing.Thing;
import org.openhab.core.types.State;
import org.openhab.binding.netatmo.internal.WeatherUtils;
import org.openhab.binding.netatmo.internal.handler.NetatmoDeviceHandler;
import org.openhab.binding.netatmo.internal.handler.NetatmoModuleHandler;
import io.swagger.client.model.NADashboardData;
import io.swagger.client.model.NAMain;
/**
* {@link NAMainHandler} is the base class for all current Netatmo
* weather station equipments (both modules and devices)
*
* @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 NAMainHandler extends NetatmoDeviceHandler<NAMain> {
private Map<String, Float> channelMeasurements = new ConcurrentHashMap<>();
public NAMainHandler(Thing thing, final TimeZoneProvider timeZoneProvider) {
super(thing, timeZoneProvider);
}
@Override
protected Optional<NAMain> updateReadings() {
Optional<NAMain> result = getBridgeHandler().flatMap(handler -> handler.getStationsDataBody(getId()))
.map(dataBody -> dataBody.getDevices().stream()
.filter(device -> device.getId().equalsIgnoreCase(getId())).findFirst().orElse(null));
result.ifPresent(device -> {
device.getModules().forEach(child -> childs.put(child.getId(), child));
});
updateMeasurements();
childs.keySet().forEach((childId) -> {
findNAThing(childId).map(NetatmoModuleHandler.class::cast).ifPresent(naChildModule -> {
naChildModule.updateMeasurements();
});
});
return result;
}
@Override
protected void updateProperties(NAMain deviceData) {
updateProperties(deviceData.getFirmware(), deviceData.getType());
}
@Override
public void updateMeasurements() {
updateDayMeasurements();
updateWeekMeasurements();
updateMonthMeasurements();
}
private void updateDayMeasurements() {
List<String> channels = new ArrayList<>();
List<String> types = new ArrayList<>();
addMeasurement(channels, types, CHANNEL_MIN_CO2, MIN_CO2);
addMeasurement(channels, types, CHANNEL_MAX_CO2, MAX_CO2);
addMeasurement(channels, types, CHANNEL_MIN_HUMIDITY, MIN_HUM);
addMeasurement(channels, types, CHANNEL_MAX_HUMIDITY, MAX_HUM);
addMeasurement(channels, types, CHANNEL_MIN_NOISE, MIN_NOISE);
addMeasurement(channels, types, CHANNEL_MAX_NOISE, MAX_NOISE);
addMeasurement(channels, types, CHANNEL_MIN_PRESSURE, MIN_PRESSURE);
addMeasurement(channels, types, CHANNEL_MAX_PRESSURE, MAX_PRESSURE);
addMeasurement(channels, types, CHANNEL_DATE_MIN_CO2, DATE_MIN_CO2);
addMeasurement(channels, types, CHANNEL_DATE_MAX_CO2, DATE_MAX_CO2);
addMeasurement(channels, types, CHANNEL_DATE_MIN_HUMIDITY, DATE_MIN_HUM);
addMeasurement(channels, types, CHANNEL_DATE_MAX_HUMIDITY, DATE_MAX_HUM);
addMeasurement(channels, types, CHANNEL_DATE_MIN_NOISE, DATE_MIN_NOISE);
addMeasurement(channels, types, CHANNEL_DATE_MAX_NOISE, DATE_MAX_NOISE);
addMeasurement(channels, types, CHANNEL_DATE_MIN_PRESSURE, DATE_MIN_PRESSURE);
addMeasurement(channels, types, CHANNEL_DATE_MAX_PRESSURE, DATE_MAX_PRESSURE);
if (!channels.isEmpty()) {
getMeasurements(getId(), null, ONE_DAY, types, channels, channelMeasurements);
}
}
private void updateWeekMeasurements() {
List<String> channels = new ArrayList<>();
List<String> types = new ArrayList<>();
addMeasurement(channels, types, CHANNEL_MIN_CO2_THIS_WEEK, MIN_CO2);
addMeasurement(channels, types, CHANNEL_MAX_CO2_THIS_WEEK, MAX_CO2);
addMeasurement(channels, types, CHANNEL_MIN_HUMIDITY_THIS_WEEK, MIN_HUM);
addMeasurement(channels, types, CHANNEL_MAX_HUMIDITY_THIS_WEEK, MAX_HUM);
addMeasurement(channels, types, CHANNEL_MIN_NOISE_THIS_WEEK, MIN_NOISE);
addMeasurement(channels, types, CHANNEL_MAX_NOISE_THIS_WEEK, MAX_NOISE);
addMeasurement(channels, types, CHANNEL_MIN_PRESSURE_THIS_WEEK, MIN_PRESSURE);
addMeasurement(channels, types, CHANNEL_MAX_PRESSURE_THIS_WEEK, MAX_PRESSURE);
addMeasurement(channels, types, CHANNEL_MIN_TEMP_THIS_WEEK, MIN_TEMP);
addMeasurement(channels, types, CHANNEL_MAX_TEMP_THIS_WEEK, MAX_TEMP);
addMeasurement(channels, types, CHANNEL_DATE_MIN_CO2_THIS_WEEK, DATE_MIN_CO2);
addMeasurement(channels, types, CHANNEL_DATE_MAX_CO2_THIS_WEEK, DATE_MAX_CO2);
addMeasurement(channels, types, CHANNEL_DATE_MIN_HUMIDITY_THIS_WEEK, DATE_MIN_HUM);
addMeasurement(channels, types, CHANNEL_DATE_MAX_HUMIDITY_THIS_WEEK, DATE_MAX_HUM);
addMeasurement(channels, types, CHANNEL_DATE_MIN_NOISE_THIS_WEEK, DATE_MIN_NOISE);
addMeasurement(channels, types, CHANNEL_DATE_MAX_NOISE_THIS_WEEK, DATE_MAX_NOISE);
addMeasurement(channels, types, CHANNEL_DATE_MIN_PRESSURE_THIS_WEEK, DATE_MIN_PRESSURE);
addMeasurement(channels, types, CHANNEL_DATE_MAX_PRESSURE_THIS_WEEK, DATE_MAX_PRESSURE);
addMeasurement(channels, types, CHANNEL_DATE_MIN_TEMP_THIS_WEEK, DATE_MIN_TEMP);
addMeasurement(channels, types, CHANNEL_DATE_MAX_TEMP_THIS_WEEK, DATE_MAX_TEMP);
if (!channels.isEmpty()) {
getMeasurements(getId(), null, ONE_WEEK, types, channels, channelMeasurements);
}
}
private void updateMonthMeasurements() {
List<String> channels = new ArrayList<>();
List<String> types = new ArrayList<>();
addMeasurement(channels, types, CHANNEL_MIN_CO2_THIS_MONTH, MIN_CO2);
addMeasurement(channels, types, CHANNEL_MAX_CO2_THIS_MONTH, MAX_CO2);
addMeasurement(channels, types, CHANNEL_MIN_HUMIDITY_THIS_MONTH, MIN_HUM);
addMeasurement(channels, types, CHANNEL_MAX_HUMIDITY_THIS_MONTH, MAX_HUM);
addMeasurement(channels, types, CHANNEL_MIN_NOISE_THIS_MONTH, MIN_NOISE);
addMeasurement(channels, types, CHANNEL_MAX_NOISE_THIS_MONTH, MAX_NOISE);
addMeasurement(channels, types, CHANNEL_MIN_PRESSURE_THIS_MONTH, MIN_PRESSURE);
addMeasurement(channels, types, CHANNEL_MAX_PRESSURE_THIS_MONTH, MAX_PRESSURE);
addMeasurement(channels, types, CHANNEL_MIN_TEMP_THIS_MONTH, MIN_TEMP);
addMeasurement(channels, types, CHANNEL_MAX_TEMP_THIS_MONTH, MAX_TEMP);
addMeasurement(channels, types, CHANNEL_DATE_MIN_CO2_THIS_MONTH, DATE_MIN_CO2);
addMeasurement(channels, types, CHANNEL_DATE_MAX_CO2_THIS_MONTH, DATE_MAX_CO2);
addMeasurement(channels, types, CHANNEL_DATE_MIN_HUMIDITY_THIS_MONTH, DATE_MIN_HUM);
addMeasurement(channels, types, CHANNEL_DATE_MAX_HUMIDITY_THIS_MONTH, DATE_MAX_HUM);
addMeasurement(channels, types, CHANNEL_DATE_MIN_NOISE_THIS_MONTH, DATE_MIN_NOISE);
addMeasurement(channels, types, CHANNEL_DATE_MAX_NOISE_THIS_MONTH, DATE_MAX_NOISE);
addMeasurement(channels, types, CHANNEL_DATE_MIN_PRESSURE_THIS_MONTH, DATE_MIN_PRESSURE);
addMeasurement(channels, types, CHANNEL_DATE_MAX_PRESSURE_THIS_MONTH, DATE_MAX_PRESSURE);
addMeasurement(channels, types, CHANNEL_DATE_MIN_TEMP_THIS_MONTH, DATE_MIN_TEMP);
addMeasurement(channels, types, CHANNEL_DATE_MAX_TEMP_THIS_MONTH, DATE_MAX_TEMP);
if (!channels.isEmpty()) {
getMeasurements(getId(), null, ONE_MONTH, types, channels, channelMeasurements);
}
}
@Override
protected State getNAThingProperty(String channelId) {
NADashboardData dashboardData = getDevice().map(d -> d.getDashboardData()).orElse(null);
if (dashboardData != null) {
switch (channelId) {
case CHANNEL_CO2:
return toQuantityType(dashboardData.getCO2(), API_CO2_UNIT);
case CHANNEL_TEMPERATURE:
return toQuantityType(dashboardData.getTemperature(), API_TEMPERATURE_UNIT);
case CHANNEL_MIN_TEMP:
return toQuantityType(dashboardData.getMinTemp(), API_TEMPERATURE_UNIT);
case CHANNEL_MAX_TEMP:
return toQuantityType(dashboardData.getMaxTemp(), API_TEMPERATURE_UNIT);
case CHANNEL_TEMP_TREND:
return toStringType(dashboardData.getTempTrend());
case CHANNEL_NOISE:
return toQuantityType(dashboardData.getNoise(), API_NOISE_UNIT);
case CHANNEL_PRESSURE:
return toQuantityType(dashboardData.getPressure(), API_PRESSURE_UNIT);
case CHANNEL_PRESS_TREND:
return toStringType(dashboardData.getPressureTrend());
case CHANNEL_ABSOLUTE_PRESSURE:
return toQuantityType(dashboardData.getAbsolutePressure(), API_PRESSURE_UNIT);
case CHANNEL_TIMEUTC:
return toDateTimeType(dashboardData.getTimeUtc(), timeZoneProvider.getTimeZone());
case CHANNEL_DATE_MIN_TEMP:
return toDateTimeType(dashboardData.getDateMinTemp(), timeZoneProvider.getTimeZone());
case CHANNEL_DATE_MAX_TEMP:
return toDateTimeType(dashboardData.getDateMaxTemp(), timeZoneProvider.getTimeZone());
case CHANNEL_HUMIDITY:
return toQuantityType(dashboardData.getHumidity(), API_HUMIDITY_UNIT);
case CHANNEL_HUMIDEX:
return toDecimalType(
WeatherUtils.getHumidex(dashboardData.getTemperature(), dashboardData.getHumidity()));
case CHANNEL_HEATINDEX:
return toQuantityType(
WeatherUtils.getHeatIndex(dashboardData.getTemperature(), dashboardData.getHumidity()),
API_TEMPERATURE_UNIT);
case CHANNEL_DEWPOINT:
return toQuantityType(
WeatherUtils.getDewPoint(dashboardData.getTemperature(), dashboardData.getHumidity()),
API_TEMPERATURE_UNIT);
case CHANNEL_DEWPOINTDEP:
Double dewPoint = WeatherUtils.getDewPoint(dashboardData.getTemperature(),
dashboardData.getHumidity());
return toQuantityType(WeatherUtils.getDewPointDep(dashboardData.getTemperature(), dewPoint),
API_TEMPERATURE_UNIT);
}
}
switch (channelId) {
case CHANNEL_MIN_CO2:
case CHANNEL_MIN_CO2_THIS_WEEK:
case CHANNEL_MIN_CO2_THIS_MONTH:
case CHANNEL_MAX_CO2:
case CHANNEL_MAX_CO2_THIS_WEEK:
case CHANNEL_MAX_CO2_THIS_MONTH:
return toQuantityType(channelMeasurements.get(channelId), API_CO2_UNIT);
case CHANNEL_MIN_HUMIDITY:
case CHANNEL_MIN_HUMIDITY_THIS_WEEK:
case CHANNEL_MIN_HUMIDITY_THIS_MONTH:
case CHANNEL_MAX_HUMIDITY:
case CHANNEL_MAX_HUMIDITY_THIS_WEEK:
case CHANNEL_MAX_HUMIDITY_THIS_MONTH:
return toQuantityType(channelMeasurements.get(channelId), API_HUMIDITY_UNIT);
case CHANNEL_MIN_NOISE:
case CHANNEL_MIN_NOISE_THIS_WEEK:
case CHANNEL_MIN_NOISE_THIS_MONTH:
case CHANNEL_MAX_NOISE:
case CHANNEL_MAX_NOISE_THIS_WEEK:
case CHANNEL_MAX_NOISE_THIS_MONTH:
return toQuantityType(channelMeasurements.get(channelId), API_NOISE_UNIT);
case CHANNEL_MIN_PRESSURE:
case CHANNEL_MIN_PRESSURE_THIS_WEEK:
case CHANNEL_MIN_PRESSURE_THIS_MONTH:
case CHANNEL_MAX_PRESSURE:
case CHANNEL_MAX_PRESSURE_THIS_WEEK:
case CHANNEL_MAX_PRESSURE_THIS_MONTH:
return toQuantityType(channelMeasurements.get(channelId), API_PRESSURE_UNIT);
case CHANNEL_MIN_TEMP_THIS_WEEK:
case CHANNEL_MIN_TEMP_THIS_MONTH:
case CHANNEL_MAX_TEMP_THIS_WEEK:
case CHANNEL_MAX_TEMP_THIS_MONTH:
return toQuantityType(channelMeasurements.get(channelId), API_TEMPERATURE_UNIT);
case CHANNEL_DATE_MIN_CO2:
case CHANNEL_DATE_MIN_CO2_THIS_WEEK:
case CHANNEL_DATE_MIN_CO2_THIS_MONTH:
case CHANNEL_DATE_MAX_CO2:
case CHANNEL_DATE_MAX_CO2_THIS_WEEK:
case CHANNEL_DATE_MAX_CO2_THIS_MONTH:
case CHANNEL_DATE_MIN_NOISE:
case CHANNEL_DATE_MIN_NOISE_THIS_WEEK:
case CHANNEL_DATE_MIN_NOISE_THIS_MONTH:
case CHANNEL_DATE_MAX_NOISE:
case CHANNEL_DATE_MAX_NOISE_THIS_WEEK:
case CHANNEL_DATE_MAX_NOISE_THIS_MONTH:
case CHANNEL_DATE_MIN_HUMIDITY:
case CHANNEL_DATE_MIN_HUMIDITY_THIS_WEEK:
case CHANNEL_DATE_MIN_HUMIDITY_THIS_MONTH:
case CHANNEL_DATE_MAX_HUMIDITY:
case CHANNEL_DATE_MAX_HUMIDITY_THIS_WEEK:
case CHANNEL_DATE_MAX_HUMIDITY_THIS_MONTH:
case CHANNEL_DATE_MIN_PRESSURE:
case CHANNEL_DATE_MIN_PRESSURE_THIS_WEEK:
case CHANNEL_DATE_MIN_PRESSURE_THIS_MONTH:
case CHANNEL_DATE_MAX_PRESSURE:
case CHANNEL_DATE_MAX_PRESSURE_THIS_WEEK:
case CHANNEL_DATE_MAX_PRESSURE_THIS_MONTH:
case CHANNEL_DATE_MIN_TEMP_THIS_WEEK:
case CHANNEL_DATE_MIN_TEMP_THIS_MONTH:
case CHANNEL_DATE_MAX_TEMP_THIS_WEEK:
case CHANNEL_DATE_MAX_TEMP_THIS_MONTH:
return toDateTimeType(channelMeasurements.get(channelId), timeZoneProvider.getTimeZone());
}
return super.getNAThingProperty(channelId);
}
@Override
protected Optional<Integer> getDataTimestamp() {
return getDevice().map(d -> d.getLastStatusStore());
}
@Override
protected boolean isReachable() {
boolean result = false;
Optional<NAMain> device = getDevice();
if (device.isPresent()) {
Boolean reachable = device.get().getReachable();
result = reachable != null ? reachable.booleanValue() : false;
}
return result;
}
}

View File

@@ -0,0 +1,185 @@
/**
* Copyright (c) 2010-2020 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.station;
import static org.openhab.binding.netatmo.internal.ChannelTypeUtils.*;
import static org.openhab.binding.netatmo.internal.NetatmoBindingConstants.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.i18n.TimeZoneProvider;
import org.openhab.core.thing.Thing;
import org.openhab.core.types.State;
import org.openhab.binding.netatmo.internal.WeatherUtils;
import org.openhab.binding.netatmo.internal.handler.NetatmoModuleHandler;
import io.swagger.client.model.NADashboardData;
import io.swagger.client.model.NAStationModule;
/**
* {@link NAModule1Handler} is the class used to handle the outdoor module
* capable of reporting temperature and humidity
*
* @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 NAModule1Handler extends NetatmoModuleHandler<NAStationModule> {
private Map<String, Float> channelMeasurements = new ConcurrentHashMap<>();
public NAModule1Handler(Thing thing, final TimeZoneProvider timeZoneProvider) {
super(thing, timeZoneProvider);
}
@Override
protected void updateProperties(NAStationModule moduleData) {
updateProperties(moduleData.getFirmware(), moduleData.getType());
}
@Override
public void updateMeasurements() {
updateDayMeasurements();
updateWeekMeasurements();
updateMonthMeasurements();
}
private void updateDayMeasurements() {
List<String> channels = new ArrayList<>();
List<String> types = new ArrayList<>();
addMeasurement(channels, types, CHANNEL_MIN_HUMIDITY, MIN_HUM);
addMeasurement(channels, types, CHANNEL_MAX_HUMIDITY, MAX_HUM);
addMeasurement(channels, types, CHANNEL_DATE_MIN_HUMIDITY, DATE_MIN_HUM);
addMeasurement(channels, types, CHANNEL_DATE_MAX_HUMIDITY, DATE_MAX_HUM);
if (!channels.isEmpty()) {
getMeasurements(getParentId(), getId(), ONE_DAY, types, channels, channelMeasurements);
}
}
private void updateWeekMeasurements() {
List<String> channels = new ArrayList<>();
List<String> types = new ArrayList<>();
addMeasurement(channels, types, CHANNEL_MIN_HUMIDITY_THIS_WEEK, MIN_HUM);
addMeasurement(channels, types, CHANNEL_MAX_HUMIDITY_THIS_WEEK, MAX_HUM);
addMeasurement(channels, types, CHANNEL_MIN_TEMP_THIS_WEEK, MIN_TEMP);
addMeasurement(channels, types, CHANNEL_MAX_TEMP_THIS_WEEK, MAX_TEMP);
addMeasurement(channels, types, CHANNEL_DATE_MIN_HUMIDITY_THIS_WEEK, DATE_MIN_HUM);
addMeasurement(channels, types, CHANNEL_DATE_MAX_HUMIDITY_THIS_WEEK, DATE_MAX_HUM);
addMeasurement(channels, types, CHANNEL_DATE_MIN_TEMP_THIS_WEEK, DATE_MIN_TEMP);
addMeasurement(channels, types, CHANNEL_DATE_MAX_TEMP_THIS_WEEK, DATE_MAX_TEMP);
if (!channels.isEmpty()) {
getMeasurements(getParentId(), getId(), ONE_WEEK, types, channels, channelMeasurements);
}
}
private void updateMonthMeasurements() {
List<String> channels = new ArrayList<>();
List<String> types = new ArrayList<>();
addMeasurement(channels, types, CHANNEL_MIN_HUMIDITY_THIS_MONTH, MIN_HUM);
addMeasurement(channels, types, CHANNEL_MAX_HUMIDITY_THIS_MONTH, MAX_HUM);
addMeasurement(channels, types, CHANNEL_MIN_TEMP_THIS_MONTH, MIN_TEMP);
addMeasurement(channels, types, CHANNEL_MAX_TEMP_THIS_MONTH, MAX_TEMP);
addMeasurement(channels, types, CHANNEL_DATE_MIN_HUMIDITY_THIS_MONTH, DATE_MIN_HUM);
addMeasurement(channels, types, CHANNEL_DATE_MAX_HUMIDITY_THIS_MONTH, DATE_MAX_HUM);
addMeasurement(channels, types, CHANNEL_DATE_MIN_TEMP_THIS_MONTH, DATE_MIN_TEMP);
addMeasurement(channels, types, CHANNEL_DATE_MAX_TEMP_THIS_MONTH, DATE_MAX_TEMP);
if (!channels.isEmpty()) {
getMeasurements(getParentId(), getId(), ONE_MONTH, types, channels, channelMeasurements);
}
}
@Override
protected State getNAThingProperty(String channelId) {
NADashboardData dashboardData = getModule().map(m -> m.getDashboardData()).orElse(null);
if (dashboardData != null) {
switch (channelId) {
case CHANNEL_TEMP_TREND:
return toStringType(dashboardData.getTempTrend());
case CHANNEL_TEMPERATURE:
return toQuantityType(dashboardData.getTemperature(), API_TEMPERATURE_UNIT);
case CHANNEL_DATE_MIN_TEMP:
return toDateTimeType(dashboardData.getDateMinTemp(), timeZoneProvider.getTimeZone());
case CHANNEL_DATE_MAX_TEMP:
return toDateTimeType(dashboardData.getDateMaxTemp(), timeZoneProvider.getTimeZone());
case CHANNEL_MIN_TEMP:
return toQuantityType(dashboardData.getMinTemp(), API_TEMPERATURE_UNIT);
case CHANNEL_MAX_TEMP:
return toQuantityType(dashboardData.getMaxTemp(), API_TEMPERATURE_UNIT);
case CHANNEL_HUMIDITY:
return toQuantityType(dashboardData.getHumidity(), API_HUMIDITY_UNIT);
case CHANNEL_TIMEUTC:
return toDateTimeType(dashboardData.getTimeUtc(), timeZoneProvider.getTimeZone());
case CHANNEL_HUMIDEX:
return toDecimalType(
WeatherUtils.getHumidex(dashboardData.getTemperature(), dashboardData.getHumidity()));
case CHANNEL_HEATINDEX:
return toQuantityType(
WeatherUtils.getHeatIndex(dashboardData.getTemperature(), dashboardData.getHumidity()),
API_TEMPERATURE_UNIT);
case CHANNEL_DEWPOINT:
return toQuantityType(
WeatherUtils.getDewPoint(dashboardData.getTemperature(), dashboardData.getHumidity()),
API_TEMPERATURE_UNIT);
case CHANNEL_DEWPOINTDEP:
Double dewpoint = WeatherUtils.getDewPoint(dashboardData.getTemperature(),
dashboardData.getHumidity());
return toQuantityType(WeatherUtils.getDewPointDep(dashboardData.getTemperature(), dewpoint),
API_TEMPERATURE_UNIT);
}
}
switch (channelId) {
case CHANNEL_MIN_HUMIDITY:
case CHANNEL_MIN_HUMIDITY_THIS_WEEK:
case CHANNEL_MIN_HUMIDITY_THIS_MONTH:
case CHANNEL_MAX_HUMIDITY:
case CHANNEL_MAX_HUMIDITY_THIS_WEEK:
case CHANNEL_MAX_HUMIDITY_THIS_MONTH:
return toQuantityType(channelMeasurements.get(channelId), API_HUMIDITY_UNIT);
case CHANNEL_MIN_TEMP_THIS_WEEK:
case CHANNEL_MIN_TEMP_THIS_MONTH:
case CHANNEL_MAX_TEMP_THIS_WEEK:
case CHANNEL_MAX_TEMP_THIS_MONTH:
return toQuantityType(channelMeasurements.get(channelId), API_TEMPERATURE_UNIT);
case CHANNEL_DATE_MIN_HUMIDITY:
case CHANNEL_DATE_MIN_HUMIDITY_THIS_WEEK:
case CHANNEL_DATE_MIN_HUMIDITY_THIS_MONTH:
case CHANNEL_DATE_MAX_HUMIDITY:
case CHANNEL_DATE_MAX_HUMIDITY_THIS_WEEK:
case CHANNEL_DATE_MAX_HUMIDITY_THIS_MONTH:
case CHANNEL_DATE_MIN_TEMP_THIS_WEEK:
case CHANNEL_DATE_MIN_TEMP_THIS_MONTH:
case CHANNEL_DATE_MAX_TEMP_THIS_WEEK:
case CHANNEL_DATE_MAX_TEMP_THIS_MONTH:
return toDateTimeType(channelMeasurements.get(channelId), timeZoneProvider.getTimeZone());
}
return super.getNAThingProperty(channelId);
}
@Override
protected boolean isReachable() {
boolean result = false;
Optional<NAStationModule> module = getModule();
if (module.isPresent()) {
Boolean reachable = module.get().getReachable();
result = reachable != null ? reachable.booleanValue() : false;
}
return result;
}
}

View File

@@ -0,0 +1,81 @@
/**
* Copyright (c) 2010-2020 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.station;
import static org.openhab.binding.netatmo.internal.ChannelTypeUtils.*;
import static org.openhab.binding.netatmo.internal.NetatmoBindingConstants.*;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.i18n.TimeZoneProvider;
import org.openhab.core.thing.Thing;
import org.openhab.core.types.State;
import org.openhab.binding.netatmo.internal.handler.NetatmoModuleHandler;
import io.swagger.client.model.NADashboardData;
import io.swagger.client.model.NAStationModule;
/**
* {@link NAModule2Handler} is the class used to handle the wind module
* capable of reporting wind angle and strength
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class NAModule2Handler extends NetatmoModuleHandler<NAStationModule> {
public NAModule2Handler(Thing thing, final TimeZoneProvider timeZoneProvider) {
super(thing, timeZoneProvider);
}
@Override
protected void updateProperties(NAStationModule moduleData) {
updateProperties(moduleData.getFirmware(), moduleData.getType());
}
@Override
protected State getNAThingProperty(String channelId) {
NADashboardData dashboardData = getModule().map(m -> m.getDashboardData()).orElse(null);
if (dashboardData != null) {
switch (channelId) {
case CHANNEL_WIND_ANGLE:
return toQuantityType(dashboardData.getWindAngle(), API_WIND_DIRECTION_UNIT);
case CHANNEL_WIND_STRENGTH:
return toQuantityType(dashboardData.getWindStrength(), API_WIND_SPEED_UNIT);
case CHANNEL_GUST_ANGLE:
return toQuantityType(dashboardData.getGustAngle(), API_WIND_DIRECTION_UNIT);
case CHANNEL_GUST_STRENGTH:
return toQuantityType(dashboardData.getGustStrength(), API_WIND_SPEED_UNIT);
case CHANNEL_TIMEUTC:
return toDateTimeType(dashboardData.getTimeUtc(), timeZoneProvider.getTimeZone());
case CHANNEL_MAX_WIND_STRENGTH:
return toQuantityType(dashboardData.getMaxWindStr(), API_WIND_SPEED_UNIT);
case CHANNEL_DATE_MAX_WIND_STRENGTH:
return toDateTimeType(dashboardData.getDateMaxWindStr(), timeZoneProvider.getTimeZone());
}
}
return super.getNAThingProperty(channelId);
}
@Override
protected boolean isReachable() {
boolean result = false;
Optional<NAStationModule> module = getModule();
if (module.isPresent()) {
Boolean reachable = module.get().getReachable();
result = reachable != null ? reachable.booleanValue() : false;
}
return result;
}
}

View File

@@ -0,0 +1,104 @@
/**
* Copyright (c) 2010-2020 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.station;
import static org.openhab.binding.netatmo.internal.ChannelTypeUtils.*;
import static org.openhab.binding.netatmo.internal.NetatmoBindingConstants.*;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.i18n.TimeZoneProvider;
import org.openhab.core.thing.Thing;
import org.openhab.core.types.State;
import org.openhab.binding.netatmo.internal.handler.NetatmoModuleHandler;
import io.swagger.client.model.NADashboardData;
import io.swagger.client.model.NAStationModule;
/**
* {@link NAModule3Handler} is the class used to handle the Rain Gauge
* capable of measuring precipitation
*
* @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 NAModule3Handler extends NetatmoModuleHandler<NAStationModule> {
private Map<String, Float> channelMeasurements = new ConcurrentHashMap<>();
public NAModule3Handler(Thing thing, final TimeZoneProvider timeZoneProvider) {
super(thing, timeZoneProvider);
}
@Override
protected void updateProperties(NAStationModule moduleData) {
updateProperties(moduleData.getFirmware(), moduleData.getType());
}
@Override
public void updateMeasurements() {
List<String> types = Arrays.asList(SUM_RAIN);
if (isLinked(CHANNEL_SUM_RAIN_THIS_WEEK)) {
getMeasurements(getParentId(), getId(), ONE_WEEK, types, Arrays.asList(CHANNEL_SUM_RAIN_THIS_WEEK),
channelMeasurements);
}
if (isLinked(CHANNEL_SUM_RAIN_THIS_MONTH)) {
getMeasurements(getParentId(), getId(), ONE_MONTH, types, Arrays.asList(CHANNEL_SUM_RAIN_THIS_MONTH),
channelMeasurements);
}
}
@Override
protected State getNAThingProperty(String channelId) {
NADashboardData dashboardData = getModule().map(m -> m.getDashboardData()).orElse(null);
if (dashboardData != null) {
switch (channelId) {
case CHANNEL_RAIN:
return toQuantityType(dashboardData.getRain(), API_RAIN_UNIT);
case CHANNEL_SUM_RAIN1:
return toQuantityType(dashboardData.getSumRain1(), API_RAIN_UNIT);
case CHANNEL_SUM_RAIN24:
return toQuantityType(dashboardData.getSumRain24(), API_RAIN_UNIT);
case CHANNEL_TIMEUTC:
return toDateTimeType(dashboardData.getTimeUtc(), timeZoneProvider.getTimeZone());
}
}
switch (channelId) {
case CHANNEL_SUM_RAIN_THIS_WEEK:
case CHANNEL_SUM_RAIN_THIS_MONTH:
return toQuantityType(channelMeasurements.get(channelId), API_RAIN_UNIT);
}
return super.getNAThingProperty(channelId);
}
@Override
protected boolean isReachable() {
boolean result = false;
Optional<NAStationModule> module = getModule();
if (module.isPresent()) {
Boolean reachable = module.get().getReachable();
result = reachable != null ? reachable.booleanValue() : false;
}
return result;
}
}

View File

@@ -0,0 +1,212 @@
/**
* Copyright (c) 2010-2020 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.station;
import static org.openhab.binding.netatmo.internal.ChannelTypeUtils.*;
import static org.openhab.binding.netatmo.internal.NetatmoBindingConstants.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.i18n.TimeZoneProvider;
import org.openhab.core.thing.Thing;
import org.openhab.core.types.State;
import org.openhab.binding.netatmo.internal.WeatherUtils;
import org.openhab.binding.netatmo.internal.handler.NetatmoModuleHandler;
import io.swagger.client.model.NADashboardData;
import io.swagger.client.model.NAStationModule;
/**
* {@link NAModule4Handler} is the class used to handle the additional
* indoor module capable of reporting temperature, humidity and CO2 level
*
* @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 NAModule4Handler extends NetatmoModuleHandler<NAStationModule> {
private Map<String, Float> channelMeasurements = new ConcurrentHashMap<>();
public NAModule4Handler(Thing thing, final TimeZoneProvider timeZoneProvider) {
super(thing, timeZoneProvider);
}
@Override
protected void updateProperties(NAStationModule moduleData) {
updateProperties(moduleData.getFirmware(), moduleData.getType());
}
@Override
public void updateMeasurements() {
updateDayMeasurements();
updateWeekMeasurements();
updateMonthMeasurements();
}
private void updateDayMeasurements() {
List<String> channels = new ArrayList<>();
List<String> types = new ArrayList<>();
addMeasurement(channels, types, CHANNEL_MIN_CO2, MIN_CO2);
addMeasurement(channels, types, CHANNEL_MAX_CO2, MAX_CO2);
addMeasurement(channels, types, CHANNEL_MIN_HUMIDITY, MIN_HUM);
addMeasurement(channels, types, CHANNEL_MAX_HUMIDITY, MAX_HUM);
addMeasurement(channels, types, CHANNEL_DATE_MIN_CO2, DATE_MIN_CO2);
addMeasurement(channels, types, CHANNEL_DATE_MAX_CO2, DATE_MAX_CO2);
addMeasurement(channels, types, CHANNEL_DATE_MIN_HUMIDITY, DATE_MIN_HUM);
addMeasurement(channels, types, CHANNEL_DATE_MAX_HUMIDITY, DATE_MAX_HUM);
if (!channels.isEmpty()) {
getMeasurements(getParentId(), getId(), ONE_DAY, types, channels, channelMeasurements);
}
}
private void updateWeekMeasurements() {
List<String> channels = new ArrayList<>();
List<String> types = new ArrayList<>();
addMeasurement(channels, types, CHANNEL_MIN_CO2_THIS_WEEK, MIN_CO2);
addMeasurement(channels, types, CHANNEL_MAX_CO2_THIS_WEEK, MAX_CO2);
addMeasurement(channels, types, CHANNEL_MIN_HUMIDITY_THIS_WEEK, MIN_HUM);
addMeasurement(channels, types, CHANNEL_MAX_HUMIDITY_THIS_WEEK, MAX_HUM);
addMeasurement(channels, types, CHANNEL_MIN_TEMP_THIS_WEEK, MIN_TEMP);
addMeasurement(channels, types, CHANNEL_MAX_TEMP_THIS_WEEK, MAX_TEMP);
addMeasurement(channels, types, CHANNEL_DATE_MIN_CO2_THIS_WEEK, DATE_MIN_CO2);
addMeasurement(channels, types, CHANNEL_DATE_MAX_CO2_THIS_WEEK, DATE_MAX_CO2);
addMeasurement(channels, types, CHANNEL_DATE_MIN_HUMIDITY_THIS_WEEK, DATE_MIN_HUM);
addMeasurement(channels, types, CHANNEL_DATE_MAX_HUMIDITY_THIS_WEEK, DATE_MAX_HUM);
addMeasurement(channels, types, CHANNEL_DATE_MIN_TEMP_THIS_WEEK, DATE_MIN_TEMP);
addMeasurement(channels, types, CHANNEL_DATE_MAX_TEMP_THIS_WEEK, DATE_MAX_TEMP);
if (!channels.isEmpty()) {
getMeasurements(getParentId(), getId(), ONE_WEEK, types, channels, channelMeasurements);
}
}
private void updateMonthMeasurements() {
List<String> channels = new ArrayList<>();
List<String> types = new ArrayList<>();
addMeasurement(channels, types, CHANNEL_MIN_CO2_THIS_MONTH, MIN_CO2);
addMeasurement(channels, types, CHANNEL_MAX_CO2_THIS_MONTH, MAX_CO2);
addMeasurement(channels, types, CHANNEL_MIN_HUMIDITY_THIS_MONTH, MIN_HUM);
addMeasurement(channels, types, CHANNEL_MAX_HUMIDITY_THIS_MONTH, MAX_HUM);
addMeasurement(channels, types, CHANNEL_MIN_TEMP_THIS_MONTH, MIN_TEMP);
addMeasurement(channels, types, CHANNEL_MAX_TEMP_THIS_MONTH, MAX_TEMP);
addMeasurement(channels, types, CHANNEL_DATE_MIN_CO2_THIS_MONTH, DATE_MIN_CO2);
addMeasurement(channels, types, CHANNEL_DATE_MAX_CO2_THIS_MONTH, DATE_MAX_CO2);
addMeasurement(channels, types, CHANNEL_DATE_MIN_HUMIDITY_THIS_MONTH, DATE_MIN_HUM);
addMeasurement(channels, types, CHANNEL_DATE_MAX_HUMIDITY_THIS_MONTH, DATE_MAX_HUM);
addMeasurement(channels, types, CHANNEL_DATE_MIN_TEMP_THIS_MONTH, DATE_MIN_TEMP);
addMeasurement(channels, types, CHANNEL_DATE_MAX_TEMP_THIS_MONTH, DATE_MAX_TEMP);
if (!channels.isEmpty()) {
getMeasurements(getParentId(), getId(), ONE_MONTH, types, channels, channelMeasurements);
}
}
@Override
protected State getNAThingProperty(String channelId) {
NADashboardData dashboardData = getModule().map(m -> m.getDashboardData()).orElse(null);
if (dashboardData != null) {
switch (channelId) {
case CHANNEL_TEMP_TREND:
return toStringType(dashboardData.getTempTrend());
case CHANNEL_CO2:
return toQuantityType(dashboardData.getCO2(), API_CO2_UNIT);
case CHANNEL_TEMPERATURE:
return toQuantityType(dashboardData.getTemperature(), API_TEMPERATURE_UNIT);
case CHANNEL_DATE_MIN_TEMP:
return toDateTimeType(dashboardData.getDateMinTemp(), timeZoneProvider.getTimeZone());
case CHANNEL_DATE_MAX_TEMP:
return toDateTimeType(dashboardData.getDateMaxTemp(), timeZoneProvider.getTimeZone());
case CHANNEL_MIN_TEMP:
return toQuantityType(dashboardData.getMinTemp(), API_TEMPERATURE_UNIT);
case CHANNEL_MAX_TEMP:
return toQuantityType(dashboardData.getMaxTemp(), API_TEMPERATURE_UNIT);
case CHANNEL_TIMEUTC:
return toDateTimeType(dashboardData.getTimeUtc(), timeZoneProvider.getTimeZone());
case CHANNEL_HUMIDITY:
return toQuantityType(dashboardData.getHumidity(), API_HUMIDITY_UNIT);
case CHANNEL_HUMIDEX:
return toDecimalType(
WeatherUtils.getHumidex(dashboardData.getTemperature(), dashboardData.getHumidity()));
case CHANNEL_HEATINDEX:
return toQuantityType(
WeatherUtils.getHeatIndex(dashboardData.getTemperature(), dashboardData.getHumidity()),
API_TEMPERATURE_UNIT);
case CHANNEL_DEWPOINT:
return toQuantityType(
WeatherUtils.getDewPoint(dashboardData.getTemperature(), dashboardData.getHumidity()),
API_TEMPERATURE_UNIT);
case CHANNEL_DEWPOINTDEP:
Double dewpoint = WeatherUtils.getDewPoint(dashboardData.getTemperature(),
dashboardData.getHumidity());
return toQuantityType(WeatherUtils.getDewPointDep(dashboardData.getTemperature(), dewpoint),
API_TEMPERATURE_UNIT);
}
}
switch (channelId) {
case CHANNEL_MIN_CO2:
case CHANNEL_MIN_CO2_THIS_WEEK:
case CHANNEL_MIN_CO2_THIS_MONTH:
case CHANNEL_MAX_CO2:
case CHANNEL_MAX_CO2_THIS_WEEK:
case CHANNEL_MAX_CO2_THIS_MONTH:
return toQuantityType(channelMeasurements.get(channelId), API_CO2_UNIT);
case CHANNEL_MIN_HUMIDITY:
case CHANNEL_MIN_HUMIDITY_THIS_WEEK:
case CHANNEL_MIN_HUMIDITY_THIS_MONTH:
case CHANNEL_MAX_HUMIDITY:
case CHANNEL_MAX_HUMIDITY_THIS_WEEK:
case CHANNEL_MAX_HUMIDITY_THIS_MONTH:
return toQuantityType(channelMeasurements.get(channelId), API_HUMIDITY_UNIT);
case CHANNEL_MIN_TEMP_THIS_WEEK:
case CHANNEL_MIN_TEMP_THIS_MONTH:
case CHANNEL_MAX_TEMP_THIS_WEEK:
case CHANNEL_MAX_TEMP_THIS_MONTH:
return toQuantityType(channelMeasurements.get(channelId), API_TEMPERATURE_UNIT);
case CHANNEL_DATE_MIN_CO2:
case CHANNEL_DATE_MIN_CO2_THIS_WEEK:
case CHANNEL_DATE_MIN_CO2_THIS_MONTH:
case CHANNEL_DATE_MAX_CO2:
case CHANNEL_DATE_MAX_CO2_THIS_WEEK:
case CHANNEL_DATE_MAX_CO2_THIS_MONTH:
case CHANNEL_DATE_MIN_HUMIDITY:
case CHANNEL_DATE_MIN_HUMIDITY_THIS_WEEK:
case CHANNEL_DATE_MIN_HUMIDITY_THIS_MONTH:
case CHANNEL_DATE_MAX_HUMIDITY:
case CHANNEL_DATE_MAX_HUMIDITY_THIS_WEEK:
case CHANNEL_DATE_MAX_HUMIDITY_THIS_MONTH:
case CHANNEL_DATE_MIN_TEMP_THIS_WEEK:
case CHANNEL_DATE_MIN_TEMP_THIS_MONTH:
case CHANNEL_DATE_MAX_TEMP_THIS_WEEK:
case CHANNEL_DATE_MAX_TEMP_THIS_MONTH:
return toDateTimeType(channelMeasurements.get(channelId), timeZoneProvider.getTimeZone());
}
return super.getNAThingProperty(channelId);
}
@Override
protected boolean isReachable() {
boolean result = false;
Optional<NAStationModule> module = getModule();
if (module.isPresent()) {
Boolean reachable = module.get().getReachable();
result = reachable != null ? reachable.booleanValue() : false;
}
return result;
}
}

View File

@@ -0,0 +1,91 @@
/**
* Copyright (c) 2010-2020 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.thermostat;
import static org.openhab.binding.netatmo.internal.ChannelTypeUtils.*;
import static org.openhab.binding.netatmo.internal.NetatmoBindingConstants.*;
import java.time.ZonedDateTime;
import java.time.temporal.TemporalAdjusters;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.i18n.TimeZoneProvider;
import org.openhab.core.thing.Thing;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import org.openhab.binding.netatmo.internal.handler.NetatmoDeviceHandler;
import io.swagger.client.model.NAPlug;
import io.swagger.client.model.NAYearMonth;
/**
* {@link NAPlugHandler} is the class used to handle the plug
* device of a thermostat set
*
* @author Gaël L'hopital - Initial contribution OH2 version
*
*/
@NonNullByDefault
public class NAPlugHandler extends NetatmoDeviceHandler<NAPlug> {
public NAPlugHandler(Thing thing, final TimeZoneProvider timeZoneProvider) {
super(thing, timeZoneProvider);
}
@Override
protected Optional<NAPlug> updateReadings() {
Optional<NAPlug> result = getBridgeHandler().flatMap(handler -> handler.getThermostatsDataBody(getId()))
.map(dataBody -> dataBody.getDevices().stream()
.filter(device -> device.getId().equalsIgnoreCase(getId())).findFirst().orElse(null));
result.ifPresent(device -> {
device.getModules().forEach(child -> childs.put(child.getId(), child));
});
return result;
}
@Override
protected void updateProperties(NAPlug deviceData) {
updateProperties(deviceData.getFirmware(), deviceData.getType());
}
@Override
protected State getNAThingProperty(String channelId) {
switch (channelId) {
case CHANNEL_CONNECTED_BOILER:
return getDevice().map(d -> toOnOffType(d.getPlugConnectedBoiler())).orElse(UnDefType.UNDEF);
case CHANNEL_LAST_PLUG_SEEN:
return getDevice().map(d -> toDateTimeType(d.getLastPlugSeen(), timeZoneProvider.getTimeZone()))
.orElse(UnDefType.UNDEF);
case CHANNEL_LAST_BILAN:
return toDateTimeType(getLastBilan());
}
return super.getNAThingProperty(channelId);
}
public @Nullable ZonedDateTime getLastBilan() {
Optional<NAYearMonth> lastBilan = getDevice().map(d -> d.getLastBilan());
if (lastBilan.isPresent()) {
ZonedDateTime zonedDT = ZonedDateTime.of(lastBilan.get().getY(), lastBilan.get().getM(), 1, 0, 0, 0, 0,
ZonedDateTime.now().getZone());
return zonedDT.with(TemporalAdjusters.lastDayOfMonth());
}
return null;
}
@Override
protected Optional<Integer> getDataTimestamp() {
return getDevice().map(d -> d.getLastStatusStore());
}
}

View File

@@ -0,0 +1,330 @@
/**
* Copyright (c) 2010-2020 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.thermostat;
import static org.openhab.binding.netatmo.internal.ChannelTypeUtils.*;
import static org.openhab.binding.netatmo.internal.NetatmoBindingConstants.*;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;
import javax.measure.quantity.Temperature;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.i18n.TimeZoneProvider;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.State;
import org.openhab.core.types.StateOption;
import org.openhab.core.types.UnDefType;
import org.openhab.binding.netatmo.internal.NATherm1StateDescriptionProvider;
import org.openhab.binding.netatmo.internal.handler.NetatmoModuleHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.swagger.client.api.ThermostatApi;
import io.swagger.client.model.NAMeasureResponse;
import io.swagger.client.model.NASetpoint;
import io.swagger.client.model.NAThermProgram;
import io.swagger.client.model.NAThermostat;
import io.swagger.client.model.NATimeTableItem;
import io.swagger.client.model.NAZone;
/**
* {@link NATherm1Handler} is the class used to handle the thermostat
* module of a thermostat set
*
* @author Gaël L'hopital - Initial contribution OH2 version
*
*/
@NonNullByDefault
public class NATherm1Handler extends NetatmoModuleHandler<NAThermostat> {
private final Logger logger = LoggerFactory.getLogger(NATherm1Handler.class);
private final NATherm1StateDescriptionProvider stateDescriptionProvider;
public NATherm1Handler(Thing thing, NATherm1StateDescriptionProvider stateDescriptionProvider,
final TimeZoneProvider timeZoneProvider) {
super(thing, timeZoneProvider);
this.stateDescriptionProvider = stateDescriptionProvider;
}
@Override
protected void updateProperties(NAThermostat moduleData) {
updateProperties(moduleData.getFirmware(), moduleData.getType());
}
@Override
public void updateChannels(Object moduleObject) {
if (isRefreshRequired()) {
measurableChannels.getAsCsv().ifPresent(csvParams -> {
getApi().ifPresent(api -> {
NAMeasureResponse measures = api.getmeasure(getParentId(), "max", csvParams, getId(), null, null, 1,
true, true);
measurableChannels.setMeasures(measures);
});
});
setRefreshRequired(false);
}
super.updateChannels(moduleObject);
getModule().ifPresent(this::updateStateDescription);
}
private void updateStateDescription(NAThermostat thermostat) {
List<StateOption> options = new ArrayList<>();
for (NAThermProgram planning : thermostat.getThermProgramList()) {
options.add(new StateOption(planning.getProgramId(), planning.getName()));
}
stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), CHANNEL_PLANNING), options);
}
@Override
protected State getNAThingProperty(String channelId) {
Optional<NAThermostat> thermostat = getModule();
switch (channelId) {
case CHANNEL_THERM_ORIENTATION:
return thermostat.map(m -> toDecimalType(m.getThermOrientation())).orElse(UnDefType.UNDEF);
case CHANNEL_THERM_RELAY:
return thermostat.map(m -> m.getThermRelayCmd() == 100 ? (State) OnOffType.ON : (State) OnOffType.OFF)
.orElse(UnDefType.UNDEF);
case CHANNEL_TEMPERATURE:
return thermostat.map(m -> toQuantityType(m.getMeasured().getTemperature(), API_TEMPERATURE_UNIT))
.orElse(UnDefType.UNDEF);
case CHANNEL_SETPOINT_TEMP:
return getCurrentSetpoint();
case CHANNEL_TIMEUTC:
return thermostat.map(m -> toDateTimeType(m.getMeasured().getTime(), timeZoneProvider.getTimeZone()))
.orElse(UnDefType.UNDEF);
case CHANNEL_SETPOINT_END_TIME: {
if (thermostat.isPresent()) {
NASetpoint setpoint = thermostat.get().getSetpoint();
if (setpoint != null) {
Integer endTime = setpoint.getSetpointEndtime();
if (endTime == null) {
endTime = getNextProgramTime(thermostat.get().getThermProgramList());
}
return toDateTimeType(endTime, timeZoneProvider.getTimeZone());
}
return UnDefType.NULL;
}
return UnDefType.UNDEF;
}
case CHANNEL_SETPOINT_MODE:
return getSetpoint();
case CHANNEL_PLANNING: {
String currentPlanning = "-";
if (thermostat.isPresent()) {
for (NAThermProgram program : thermostat.get().getThermProgramList()) {
if (program.getSelected() == Boolean.TRUE) {
currentPlanning = program.getProgramId();
}
}
return toStringType(currentPlanning);
}
return UnDefType.UNDEF;
}
}
return super.getNAThingProperty(channelId);
}
private State getSetpoint() {
return getModule()
.map(m -> m.getSetpoint() != null ? toStringType(m.getSetpoint().getSetpointMode()) : UnDefType.NULL)
.orElse(UnDefType.UNDEF);
}
private State getCurrentSetpoint() {
Optional<NAThermostat> thermostat = getModule();
if (thermostat.isPresent()) {
NASetpoint setPoint = thermostat.get().getSetpoint();
if (setPoint != null) {
String currentMode = setPoint.getSetpointMode();
NAThermProgram currentProgram = thermostat.get().getThermProgramList().stream()
.filter(p -> p.getSelected() != null && p.getSelected()).findFirst().get();
switch (currentMode) {
case CHANNEL_SETPOINT_MODE_MANUAL:
return toDecimalType(setPoint.getSetpointTemp());
case CHANNEL_SETPOINT_MODE_AWAY:
NAZone zone = getZone(currentProgram.getZones(), 2);
return toDecimalType(zone.getTemp());
case CHANNEL_SETPOINT_MODE_HG:
NAZone zone1 = getZone(currentProgram.getZones(), 3);
return toDecimalType(zone1.getTemp());
case CHANNEL_SETPOINT_MODE_PROGRAM:
NATimeTableItem currentProgramMode = getCurrentProgramMode(
thermostat.get().getThermProgramList());
if (currentProgramMode != null) {
NAZone zone2 = getZone(currentProgram.getZones(), currentProgramMode.getId());
return toDecimalType(zone2.getTemp());
}
case CHANNEL_SETPOINT_MODE_OFF:
case CHANNEL_SETPOINT_MODE_MAX:
return UnDefType.UNDEF;
}
}
}
return UnDefType.NULL;
}
private NAZone getZone(List<NAZone> zones, int searchedId) {
return zones.stream().filter(z -> z.getId() == searchedId).findFirst().get();
}
private long getNetatmoProgramBaseTime() {
Calendar mondayZero = Calendar.getInstance();
mondayZero.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY);
mondayZero.set(Calendar.HOUR_OF_DAY, 0);
mondayZero.set(Calendar.MINUTE, 0);
mondayZero.set(Calendar.SECOND, 0);
return mondayZero.getTimeInMillis();
}
private @Nullable NATimeTableItem getCurrentProgramMode(List<NAThermProgram> thermProgramList) {
NATimeTableItem lastProgram = null;
Calendar now = Calendar.getInstance();
long diff = (now.getTimeInMillis() - getNetatmoProgramBaseTime()) / 1000 / 60;
Optional<NAThermProgram> currentProgram = thermProgramList.stream()
.filter(p -> p.getSelected() != null && p.getSelected()).findFirst();
if (currentProgram.isPresent()) {
Stream<NATimeTableItem> pastPrograms = currentProgram.get().getTimetable().stream()
.filter(t -> t.getMOffset() < diff);
Optional<NATimeTableItem> program = pastPrograms.reduce((first, second) -> second);
if (program.isPresent()) {
lastProgram = program.get();
}
}
return lastProgram;
}
private int getNextProgramTime(List<NAThermProgram> thermProgramList) {
Calendar now = Calendar.getInstance();
long diff = (now.getTimeInMillis() - getNetatmoProgramBaseTime()) / 1000 / 60;
int result = -1;
for (NAThermProgram thermProgram : thermProgramList) {
if (thermProgram.getSelected() != null && thermProgram.getSelected()) {
// By default we'll use the first slot of next week - this case will be true if
// we are in the last schedule of the week so below loop will not exit by break
int next = thermProgram.getTimetable().get(0).getMOffset() + (7 * 24 * 60);
for (NATimeTableItem timeTable : thermProgram.getTimetable()) {
if (timeTable.getMOffset() > diff) {
next = timeTable.getMOffset();
break;
}
}
result = (int) (next * 60 + (getNetatmoProgramBaseTime() / 1000));
}
}
return result;
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
super.handleCommand(channelUID, command);
if (!(command instanceof RefreshType)) {
try {
switch (channelUID.getId()) {
case CHANNEL_SETPOINT_MODE: {
String targetMode = command.toString();
if (CHANNEL_SETPOINT_MODE_MANUAL.equals(targetMode)) {
logger.info(
"Switching to manual mode is done by assigning a setpoint temperature - command dropped");
updateState(channelUID, getSetpoint());
} else {
pushSetpointUpdate(targetMode, null, null);
}
break;
}
case CHANNEL_SETPOINT_TEMP: {
BigDecimal spTemp = null;
if (command instanceof QuantityType) {
@SuppressWarnings("unchecked")
QuantityType<Temperature> quantity = ((QuantityType<Temperature>) command)
.toUnit(API_TEMPERATURE_UNIT);
if (quantity != null) {
spTemp = quantity.toBigDecimal().setScale(1, RoundingMode.HALF_UP);
}
} else {
spTemp = new BigDecimal(command.toString()).setScale(1, RoundingMode.HALF_UP);
}
if (spTemp != null) {
pushSetpointUpdate(CHANNEL_SETPOINT_MODE_MANUAL, getSetpointEndTime(), spTemp.floatValue());
}
break;
}
case CHANNEL_PLANNING: {
getApi().ifPresent(api -> {
api.switchschedule(getParentId(), getId(), command.toString());
updateState(channelUID, new StringType(command.toString()));
invalidateParentCacheAndRefresh();
});
}
}
} catch (Exception e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, e.getMessage());
}
}
}
private void pushSetpointUpdate(String target_mode, @Nullable Integer setpointEndtime,
@Nullable Float setpointTemp) {
getApi().ifPresent(api -> {
api.setthermpoint(getParentId(), getId(), target_mode, setpointEndtime, setpointTemp);
invalidateParentCacheAndRefresh();
});
}
private int getSetpointEndTime() {
Calendar cal = Calendar.getInstance();
cal.add(Calendar.MINUTE, getSetPointDefaultDuration());
cal.set(Calendar.SECOND, 0);
cal.set(Calendar.MILLISECOND, 0);
return (int) (cal.getTimeInMillis() / 1000);
}
private int getSetPointDefaultDuration() {
// TODO : this informations could be sourced from Netatmo API instead of a local configuration element
Configuration conf = config;
Object defaultDuration = conf != null ? conf.get(SETPOINT_DEFAULT_DURATION) : null;
if (defaultDuration instanceof BigDecimal) {
return ((BigDecimal) defaultDuration).intValue();
}
return 60;
}
private Optional<ThermostatApi> getApi() {
return getBridgeHandler().flatMap(handler -> handler.getThermostatApi());
}
}

View File

@@ -0,0 +1,155 @@
/**
* Copyright (c) 2010-2020 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.webhook;
import java.util.ArrayList;
import java.util.List;
import com.google.gson.annotations.SerializedName;
/**
* The {@link NAWebhookCameraEvent} is responsible to hold
* data given back by the Netatmo API when calling the webhook
*
* @author Gaël L'hopital - Initial contribution
*
*/
public class NAWebhookCameraEvent {
public enum AppTypeEnum {
@SerializedName("app_camera")
CAMERA("camera");
private String value;
AppTypeEnum(String value) {
this.value = value;
}
@Override
public String toString() {
return value;
}
}
@SerializedName("app_type")
private AppTypeEnum appType = null;
public AppTypeEnum getAppType() {
return appType;
}
public enum EventTypeEnum {
@SerializedName("person")
PERSON("person"),
@SerializedName("person_away")
PERSON_AWAY("person_away"),
@SerializedName("movement")
MOVEMENT("movement"),
@SerializedName("outdoor")
OUTDOOR("outdoor"),
@SerializedName("connection")
CONNECTION("connection"),
@SerializedName("disconnection")
DISCONNECTION("disconnection"),
@SerializedName("on")
ON("on"),
@SerializedName("off")
OFF("off"),
@SerializedName("boot")
BOOT("boot"),
@SerializedName("sd")
SD("sd"),
@SerializedName("alim")
ALIM("alim"),
@SerializedName("daily_summary")
DAILY_SUMMARY("daily_summary"),
@SerializedName("new_module")
NEW_MODULE("new_module"),
@SerializedName("module_connect")
MODULE_CONNECT("module_connect"),
@SerializedName("module_disconnect")
MODULE_DISCONNECT("module_disconnect"),
@SerializedName("module_low_battery")
MODULE_LOW_BATTERY("module_low_battery"),
@SerializedName("module_end_update")
MODULE_END_UPDATE("module_end_update"),
@SerializedName("tag_big_move")
TAG_BIG_MOVE("tag_big_move"),
@SerializedName("tag_small_move")
TAG_SMALL_MOVE("tag_small_move"),
@SerializedName("tag_uninstalled")
TAG_UNINSTALLED("tag_uninstalled"),
@SerializedName("tag_open")
TAG_OPEN("tag_open");
private String value;
EventTypeEnum(String value) {
this.value = value;
}
@Override
public String toString() {
return value;
}
}
@SerializedName("event_type")
private EventTypeEnum eventType = null;
public EventTypeEnum getEventType() {
return eventType;
}
@SerializedName("camera_id")
String cameraId;
public String getCameraId() {
return cameraId;
}
@SerializedName("home_id")
String homeId;
public String getHomeId() {
return homeId;
}
@SerializedName("persons")
private List<NAWebhookCameraEventPerson> persons = new ArrayList<>();
public List<NAWebhookCameraEventPerson> getPersons() {
return persons;
}
}

View File

@@ -0,0 +1,52 @@
/**
* Copyright (c) 2010-2020 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.webhook;
import com.google.gson.annotations.SerializedName;
/**
* The {@link NAWebhookCameraEventPerson} is responsible to hold
* data given back by the Netatmo API when calling the webhook
*
* @author Gaël L'hopital - Initial contribution
*
*/
public class NAWebhookCameraEventPerson {
@SerializedName("id")
String id;
public String getId() {
return id;
}
@SerializedName("face_id")
String faceId;
public String getFaceId() {
return faceId;
}
@SerializedName("face_key")
String faceKey;
public String getFaceKey() {
return faceKey;
}
@SerializedName("is_known")
Boolean isKnown;
public Boolean isKnown() {
return isKnown;
}
}

View File

@@ -0,0 +1,122 @@
/**
* Copyright (c) 2010-2020 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.webhook;
import java.io.IOException;
import java.util.Scanner;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.netatmo.internal.handler.NetatmoBridgeHandler;
import org.osgi.service.http.HttpService;
import org.osgi.service.http.NamespaceException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
/**
* Main OSGi service and HTTP servlet for Netatmo Welcome Webhook.
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class WelcomeWebHookServlet extends HttpServlet {
private static final long serialVersionUID = 1288539782077957954L;
private static final String PATH = "/netatmo/%s/camera";
private static final String APPLICATION_JSON = "application/json";
private static final String CHARSET = "utf-8";
private final Gson gson = new Gson();
private final Logger logger = LoggerFactory.getLogger(WelcomeWebHookServlet.class);
private HttpService httpService;
private @Nullable NetatmoBridgeHandler bridgeHandler;
private String path;
public WelcomeWebHookServlet(HttpService httpService, String id) {
this.httpService = httpService;
this.path = String.format(PATH, id);
}
/**
* OSGi activation callback.
*
* @param config Service config.
*/
public void activate(NetatmoBridgeHandler bridgeHandler) {
this.bridgeHandler = bridgeHandler;
try {
httpService.registerServlet(path, this, null, httpService.createDefaultHttpContext());
logger.debug("Started Netatmo Webhook servlet at {}", path);
} catch (ServletException | NamespaceException e) {
logger.error("Could not start Netatmo Webhook servlet: {}", e.getMessage(), e);
}
}
/**
* OSGi deactivation callback.
*/
public void deactivate() {
httpService.unregister(path);
logger.debug("Netatmo webhook servlet stopped");
this.bridgeHandler = null;
}
@Override
protected void service(@Nullable HttpServletRequest req, @Nullable HttpServletResponse resp)
throws ServletException, IOException {
if (req == null || resp == null) {
return;
}
String data = inputStreamToString(req);
NetatmoBridgeHandler handler = bridgeHandler;
if (!data.isEmpty() && handler != null) {
NAWebhookCameraEvent event = gson.fromJson(data, NAWebhookCameraEvent.class);
logger.debug("Event transmitted from restService");
handler.webHookEvent(event);
}
setHeaders(resp);
resp.getWriter().write("");
}
private String inputStreamToString(HttpServletRequest req) throws IOException {
String value = "";
try (Scanner scanner = new Scanner(req.getInputStream())) {
scanner.useDelimiter("\\A");
value = scanner.hasNext() ? scanner.next() : "";
}
return value;
}
private void setHeaders(HttpServletResponse response) {
response.setCharacterEncoding(CHARSET);
response.setContentType(APPLICATION_JSON);
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Methods", "POST");
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
}
public String getPath() {
return path;
}
}

View File

@@ -0,0 +1,56 @@
/**
* Copyright (c) 2010-2020 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.welcome;
import static org.openhab.binding.netatmo.internal.NetatmoBindingConstants.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.i18n.TimeZoneProvider;
import org.openhab.core.thing.Thing;
import org.openhab.core.types.State;
import org.openhab.binding.netatmo.internal.camera.CameraHandler;
/**
* {@link NAWelcomeCameraHandler} is the class used to handle the Welcome Camera Data
*
* @author Ing. Peter Weiss - Initial contribution
*
*/
@NonNullByDefault
public class NAWelcomeCameraHandler extends CameraHandler {
public NAWelcomeCameraHandler(Thing thing, final TimeZoneProvider timeZoneProvider) {
super(thing, timeZoneProvider);
}
@Override
protected State getNAThingProperty(String channelId) {
switch (channelId) {
case CHANNEL_WELCOME_CAMERA_STATUS:
return getStatusState();
case CHANNEL_WELCOME_CAMERA_SDSTATUS:
return getSdStatusState();
case CHANNEL_WELCOME_CAMERA_ALIMSTATUS:
return getAlimStatusState();
case CHANNEL_WELCOME_CAMERA_ISLOCAL:
return getIsLocalState();
case CHANNEL_WELCOME_CAMERA_LIVEPICTURE_URL:
return getLivePictureURLState();
case CHANNEL_WELCOME_CAMERA_LIVEPICTURE:
return getLivePictureState();
case CHANNEL_WELCOME_CAMERA_LIVESTREAM_URL:
return getLiveStreamState();
}
return super.getNAThingProperty(channelId);
}
}

View File

@@ -0,0 +1,268 @@
/**
* Copyright (c) 2010-2020 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.welcome;
import static org.openhab.binding.netatmo.internal.ChannelTypeUtils.*;
import static org.openhab.binding.netatmo.internal.NetatmoBindingConstants.*;
import java.util.Calendar;
import java.util.Comparator;
import java.util.Optional;
import java.util.Set;
import java.util.TreeSet;
import java.util.function.Function;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.i18n.TimeZoneProvider;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.Thing;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import org.openhab.binding.netatmo.internal.ChannelTypeUtils;
import org.openhab.binding.netatmo.internal.camera.CameraHandler;
import org.openhab.binding.netatmo.internal.handler.AbstractNetatmoThingHandler;
import org.openhab.binding.netatmo.internal.handler.NetatmoDeviceHandler;
import org.openhab.binding.netatmo.internal.webhook.NAWebhookCameraEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.swagger.client.model.NAWelcomeEvent;
import io.swagger.client.model.NAWelcomeHome;
import io.swagger.client.model.NAWelcomePlace;
import io.swagger.client.model.NAWelcomeSnapshot;
import io.swagger.client.model.NAWelcomeSubEvent;
/**
* {@link NAWelcomeHomeHandler} is the class used to handle the Welcome Home Data
*
* @author Gaël L'hopital - Initial contribution
* @author Ing. Peter Weiss - Welcome camera implementation
*
*/
@NonNullByDefault
public class NAWelcomeHomeHandler extends NetatmoDeviceHandler<NAWelcomeHome> {
private final Logger logger = LoggerFactory.getLogger(NAWelcomeHomeHandler.class);
private int iPersons = -1;
private int iUnknowns = -1;
private @Nullable NAWelcomeEvent lastEvent;
private boolean isNewLastEvent;
private @Nullable Integer dataTimeStamp;
public NAWelcomeHomeHandler(Thing thing, final TimeZoneProvider timeZoneProvider) {
super(thing, timeZoneProvider);
}
@Override
protected Optional<NAWelcomeHome> updateReadings() {
Optional<NAWelcomeHome> result = getBridgeHandler().flatMap(handler -> handler.getWelcomeDataBody(getId()))
.map(dataBody -> dataBody.getHomes().stream().filter(device -> device.getId().equalsIgnoreCase(getId()))
.findFirst().orElse(null));
// data time stamp is updated to now as WelcomeDataBody does not provide any information according to this need
dataTimeStamp = (int) (Calendar.getInstance().getTimeInMillis() / 1000);
result.ifPresent(home -> {
home.getCameras().forEach(camera -> childs.put(camera.getId(), camera));
// Check how many persons are at home
iPersons = 0;
iUnknowns = 0;
logger.debug("welcome home '{}' calculate Persons at home count", getId());
home.getPersons().forEach(person -> {
iPersons += person.getOutOfSight() ? 0 : 1;
if (person.getPseudo() != null) {
childs.put(person.getId(), person);
} else {
iUnknowns += person.getOutOfSight() ? 0 : 1;
}
});
NAWelcomeEvent previousLastEvent = lastEvent;
lastEvent = home.getEvents().stream().max(Comparator.comparingInt(NAWelcomeEvent::getTime)).orElse(null);
isNewLastEvent = previousLastEvent != null && !previousLastEvent.equals(lastEvent);
});
return result;
}
@Override
protected State getNAThingProperty(String channelId) {
Optional<NAWelcomeEvent> lastEvt = getLastEvent();
switch (channelId) {
case CHANNEL_WELCOME_HOME_CITY:
return getPlaceInfo(NAWelcomePlace::getCity);
case CHANNEL_WELCOME_HOME_COUNTRY:
return getPlaceInfo(NAWelcomePlace::getCountry);
case CHANNEL_WELCOME_HOME_TIMEZONE:
return getPlaceInfo(NAWelcomePlace::getTimezone);
case CHANNEL_WELCOME_HOME_PERSONCOUNT:
return iPersons != -1 ? new DecimalType(iPersons) : UnDefType.UNDEF;
case CHANNEL_WELCOME_HOME_UNKNOWNCOUNT:
return iUnknowns != -1 ? new DecimalType(iUnknowns) : UnDefType.UNDEF;
case CHANNEL_WELCOME_EVENT_TYPE:
return lastEvt.map(e -> toStringType(e.getType())).orElse(UnDefType.UNDEF);
case CHANNEL_WELCOME_EVENT_TIME:
return lastEvt.map(e -> toDateTimeType(e.getTime(), timeZoneProvider.getTimeZone()))
.orElse(UnDefType.UNDEF);
case CHANNEL_WELCOME_EVENT_CAMERAID:
if (lastEvt.isPresent()) {
return findNAThing(lastEvt.get().getCameraId()).map(c -> toStringType(c.getThing().getLabel()))
.orElse(UnDefType.UNDEF);
} else {
return UnDefType.UNDEF;
}
case CHANNEL_WELCOME_EVENT_PERSONID:
if (lastEvt.isPresent()) {
return findNAThing(lastEvt.get().getPersonId()).map(p -> toStringType(p.getThing().getLabel()))
.orElse(UnDefType.UNDEF);
} else {
return UnDefType.UNDEF;
}
case CHANNEL_WELCOME_EVENT_SNAPSHOT:
return findSnapshotURL().map(url -> toRawType(url)).orElse(UnDefType.UNDEF);
case CHANNEL_WELCOME_EVENT_SNAPSHOT_URL:
return findSnapshotURL().map(ChannelTypeUtils::toStringType).orElse(UnDefType.UNDEF);
case CHANNEL_WELCOME_EVENT_VIDEO_URL:
if (lastEvt.isPresent() && lastEvt.get().getVideoId() != null) {
String cameraId = lastEvt.get().getCameraId();
Optional<AbstractNetatmoThingHandler> thing = findNAThing(cameraId);
if (thing.isPresent()) {
CameraHandler eventCamera = (CameraHandler) thing.get();
Optional<String> streamUrl = eventCamera.getStreamURL(lastEvt.get().getVideoId());
if (streamUrl.isPresent()) {
return new StringType(streamUrl.get());
}
}
}
return UnDefType.UNDEF;
case CHANNEL_WELCOME_EVENT_VIDEOSTATUS:
return lastEvt.map(e -> e.getVideoId() != null ? toStringType(e.getVideoStatus()) : UnDefType.UNDEF)
.orElse(UnDefType.UNDEF);
case CHANNEL_WELCOME_EVENT_ISARRIVAL:
return lastEvt.map(e -> toOnOffType(e.getIsArrival())).orElse(UnDefType.UNDEF);
case CHANNEL_WELCOME_EVENT_MESSAGE:
return findEventMessage().map(m -> (State) new StringType(m.replace("<b>", "").replace("</b>", "")))
.orElse(UnDefType.UNDEF);
case CHANNEL_WELCOME_EVENT_SUBTYPE:
return lastEvt.map(e -> toDecimalType(e.getSubType())).orElse(UnDefType.UNDEF);
}
return super.getNAThingProperty(channelId);
}
@Override
protected void triggerChannelIfRequired(String channelId) {
if (isNewLastEvent) {
if (CHANNEL_CAMERA_EVENT.equals(channelId)) {
findDetectedObjectTypes(getLastEvent())
.forEach(detectedType -> triggerChannel(channelId, detectedType));
}
}
super.triggerChannelIfRequired(channelId);
}
private static Set<String> findDetectedObjectTypes(Optional<NAWelcomeEvent> eventOptional) {
Set<String> detectedObjectTypes = new TreeSet<>();
if (!eventOptional.isPresent()) {
return detectedObjectTypes;
}
NAWelcomeEvent event = eventOptional.get();
if (NAWebhookCameraEvent.EventTypeEnum.MOVEMENT.toString().equals(event.getType())) {
if (event.getPersonId() != null) {
detectedObjectTypes.add(NAWelcomeSubEvent.TypeEnum.HUMAN.name());
} else {
Optional<NAWelcomeSubEvent.TypeEnum> detectedCategory = findDetectedCategory(event);
if (detectedCategory.isPresent()) {
detectedObjectTypes.add(detectedCategory.get().name());
} else {
detectedObjectTypes.add(NAWebhookCameraEvent.EventTypeEnum.MOVEMENT.name());
}
}
}
event.getEventList().forEach(subEvent -> {
String detectedObjectType = subEvent.getType().name();
detectedObjectTypes.add(detectedObjectType);
});
return detectedObjectTypes;
}
private static Optional<NAWelcomeSubEvent.TypeEnum> findDetectedCategory(NAWelcomeEvent event) {
NAWelcomeEvent.CategoryEnum category = event.getCategory();
if (category != null) {
// It is safe to convert the enum, both enums have the same values.
return Optional.of(NAWelcomeSubEvent.TypeEnum.valueOf(category.name()));
}
return Optional.empty();
}
private Optional<String> findEventMessage() {
Optional<NAWelcomeEvent> lastEvt = getLastEvent();
if (lastEvt.isPresent()) {
@Nullable
String message = lastEvt.get().getMessage();
if (message != null) {
return Optional.of(message);
}
return lastEvt.flatMap(this::findFirstSubEvent).map(NAWelcomeSubEvent::getMessage);
}
return Optional.empty();
}
/**
* Returns the Url of the picture
*
* @return Url of the picture or null
*/
protected Optional<String> findSnapshotURL() {
Optional<NAWelcomeEvent> lastEvt = getLastEvent();
if (lastEvt.isPresent()) {
@Nullable
NAWelcomeSnapshot snapshot = lastEvt.get().getSnapshot();
if (snapshot == null) {
snapshot = lastEvt.flatMap(this::findFirstSubEvent).map(NAWelcomeSubEvent::getSnapshot).orElse(null);
}
if (snapshot != null && snapshot.getId() != null && snapshot.getKey() != null) {
return Optional.of(WELCOME_PICTURE_URL + "?" + WELCOME_PICTURE_IMAGEID + "=" + snapshot.getId() + "&"
+ WELCOME_PICTURE_KEY + "=" + snapshot.getKey());
} else {
logger.debug("Unable to build snapshot url for Home : {}", getId());
}
}
return Optional.empty();
}
@Override
protected Optional<Integer> getDataTimestamp() {
Integer timestamp = dataTimeStamp;
return timestamp != null ? Optional.of(timestamp) : Optional.empty();
}
private State getPlaceInfo(Function<NAWelcomePlace, String> infoGetFunction) {
return getDevice().map(d -> toStringType(infoGetFunction.apply(d.getPlace()))).orElse(UnDefType.UNDEF);
}
private Optional<NAWelcomeSubEvent> findFirstSubEvent(NAWelcomeEvent event) {
return Optional.ofNullable(event).map(NAWelcomeEvent::getEventList)
.flatMap(subEvents -> subEvents.stream().findFirst());
}
private Optional<NAWelcomeEvent> getLastEvent() {
NAWelcomeEvent evt = lastEvent;
return evt != null ? Optional.of(evt) : Optional.empty();
}
}

View File

@@ -0,0 +1,153 @@
/**
* Copyright (c) 2010-2020 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.welcome;
import static org.openhab.binding.netatmo.internal.ChannelTypeUtils.*;
import static org.openhab.binding.netatmo.internal.NetatmoBindingConstants.*;
import java.util.List;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.i18n.TimeZoneProvider;
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.openhab.binding.netatmo.internal.handler.NetatmoBridgeHandler;
import org.openhab.binding.netatmo.internal.handler.NetatmoModuleHandler;
import io.swagger.client.api.WelcomeApi;
import io.swagger.client.model.NAWelcomeEvent;
import io.swagger.client.model.NAWelcomeEventResponse;
import io.swagger.client.model.NAWelcomeFace;
import io.swagger.client.model.NAWelcomePerson;
/**
* {@link NAWelcomePersonHandler} is the class used to handle the Welcome Home Data
*
* @author Ing. Peter Weiss - Initial contribution
*
*/
@NonNullByDefault
public class NAWelcomePersonHandler extends NetatmoModuleHandler<NAWelcomePerson> {
private @Nullable String avatarURL;
private @Nullable NAWelcomeEvent lastEvent;
public NAWelcomePersonHandler(Thing thing, final TimeZoneProvider timeZoneProvider) {
super(thing, timeZoneProvider);
}
@Override
public void updateChannels(Object module) {
if (isRefreshRequired()) {
getApi().ifPresent(api -> {
NAWelcomeEventResponse eventResponse = api.getlasteventof(getParentId(), getId(), 10);
// Search the last event for this person
List<NAWelcomeEvent> rawEventList = eventResponse.getBody().getEventsList();
rawEventList.forEach(event -> {
if (event.getPersonId() != null && event.getPersonId().equalsIgnoreCase(getId())
&& (lastEvent == null || lastEvent.getTime() < event.getTime())) {
lastEvent = event;
}
});
});
setRefreshRequired(false);
}
super.updateChannels(module);
}
@Override
protected State getNAThingProperty(String channelId) {
Optional<NAWelcomeEvent> lastEvt = getLastEvent();
String url;
switch (channelId) {
case CHANNEL_WELCOME_PERSON_LASTSEEN:
return getModule().map(m -> toDateTimeType(m.getLastSeen(), timeZoneProvider.getTimeZone()))
.orElse(UnDefType.UNDEF);
case CHANNEL_WELCOME_PERSON_ATHOME:
return getModule()
.map(m -> m.getOutOfSight() != null ? toOnOffType(!m.getOutOfSight()) : UnDefType.UNDEF)
.orElse(UnDefType.UNDEF);
case CHANNEL_WELCOME_PERSON_AVATAR_URL:
return toStringType(getAvatarURL());
case CHANNEL_WELCOME_PERSON_AVATAR:
url = getAvatarURL();
return url != null ? toRawType(url) : UnDefType.UNDEF;
case CHANNEL_WELCOME_PERSON_LASTMESSAGE:
return (lastEvt.isPresent() && lastEvt.get().getMessage() != null)
? toStringType(lastEvt.get().getMessage().replace("<b>", "").replace("</b>", ""))
: UnDefType.UNDEF;
case CHANNEL_WELCOME_PERSON_LASTTIME:
return lastEvt.isPresent() ? toDateTimeType(lastEvt.get().getTime(), timeZoneProvider.getTimeZone())
: UnDefType.UNDEF;
case CHANNEL_WELCOME_PERSON_LASTEVENT:
url = getLastEventURL();
return url != null ? toRawType(url) : UnDefType.UNDEF;
case CHANNEL_WELCOME_PERSON_LASTEVENT_URL:
return getLastEventURL() != null ? toStringType(getLastEventURL()) : UnDefType.UNDEF;
}
return super.getNAThingProperty(channelId);
}
private @Nullable String getLastEventURL() {
Optional<NetatmoBridgeHandler> handler = getBridgeHandler();
Optional<NAWelcomeEvent> lastEvt = getLastEvent();
if (handler.isPresent() && lastEvt.isPresent() && lastEvt.get().getSnapshot() != null) {
return handler.get().getPictureUrl(lastEvt.get().getSnapshot().getId(),
lastEvt.get().getSnapshot().getKey());
}
return null;
}
private @Nullable String getAvatarURL() {
Optional<NetatmoBridgeHandler> handler = getBridgeHandler();
Optional<NAWelcomePerson> person = getModule();
if (handler.isPresent() && avatarURL == null && person.isPresent()) {
NAWelcomeFace face = person.get().getFace();
if (face != null) {
avatarURL = handler.get().getPictureUrl(face.getId(), face.getKey());
}
}
return avatarURL;
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
super.handleCommand(channelUID, command);
if ((command instanceof OnOffType) && (CHANNEL_WELCOME_PERSON_ATHOME.equalsIgnoreCase(channelUID.getId()))) {
getApi().ifPresent(api -> {
if ((OnOffType) command == OnOffType.OFF) {
api.setpersonsaway(getParentId(), getId());
} else {
api.setpersonshome(getParentId(), "[\"" + getId() + "\"]");
}
invalidateParentCacheAndRefresh();
});
}
}
private Optional<WelcomeApi> getApi() {
return getBridgeHandler().flatMap(handler -> handler.getWelcomeApi());
}
private Optional<NAWelcomeEvent> getLastEvent() {
NAWelcomeEvent evt = lastEvent;
return evt != null ? Optional.of(evt) : Optional.empty();
}
}

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="netatmo" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
<name>Netatmo Binding</name>
<description>The Netatmo binding integrates Weather Station with companion modules, Healthy Home Coach, Thermostat Plug
and Welcome Camera.</description>
<author>Gaël L'hopital and Ing. Peter Weiss</author>
<config-description>
<parameter name="backgroundDiscovery" type="boolean">
<label>Background Discovery</label>
<description>If set to true, the device and its associated modules are updated in the discovery inbox at each API
call run to refresh device data. Default is false.</description>
<required>false</required>
<default>false</default>
</parameter>
</config-description>
</binding:binding>

View File

@@ -0,0 +1,173 @@
<?xml version="1.0" encoding="UTF-8"?>
<config-description:config-descriptions
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0
https://openhab.org/schemas/config-description-1.0.0.xsd">
<config-description uri="thing-type:netatmo:bridge">
<parameter name="clientId" type="text">
<label>Client ID</label>
<description>Client ID provided for the application you created on http://dev.netatmo.com/createapp</description>
<required>true</required>
</parameter>
<parameter name="clientSecret" type="text">
<label>Client Secret</label>
<description>Client Secret provided for the application you created</description>
<required>true</required>
<context>password</context>
</parameter>
<parameter name="username" type="text">
<label>Username</label>
<description>Your Netatmo API username (email)</description>
<required>true</required>
</parameter>
<parameter name="password" type="text">
<label>Password</label>
<description>Your Netatmo API password</description>
<required>true</required>
<context>password</context>
</parameter>
<parameter name="readStation" type="boolean" required="true">
<label>Access Weather Station</label>
<description>Read weather station's data.</description>
<default>true</default>
</parameter>
<parameter name="readHealthyHomeCoach" type="boolean" required="true">
<label>Access Healthy Home Coach</label>
<description>Read healthy home coach's data.</description>
<default>false</default>
</parameter>
<parameter name="readThermostat" type="boolean" required="true">
<label>Access Thermostat</label>
<description>Read and Write thermostat's data.</description>
<default>false</default>
</parameter>
<parameter name="readWelcome" type="boolean" required="true">
<label>Access Welcome Camera</label>
<description>Read and Access Welcome camera's data.</description>
<default>false</default>
</parameter>
<parameter name="readPresence" type="boolean" required="true">
<label>Access Presence Camera</label>
<description>Read and Access Presence camera's data.</description>
<default>false</default>
</parameter>
<parameter name="webHookUrl" type="text" required="false">
<label>Webhook Address</label>
<description>Protocol, public IP and port to access OH2 server from Internet.</description>
<advanced>true</advanced>
</parameter>
<parameter name="reconnectInterval" type="integer" unit="s" required="true">
<label>Reconnect Interval</label>
<description>The reconnection interval to Netatmo API (in s).</description>
<default>5400</default>
<advanced>true</advanced>
</parameter>
</config-description>
<config-description uri="thing-type:netatmo:station">
<parameter name="id" type="text" pattern="([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})">
<label>Equipment ID</label>
<description>Id of the Device (mac address)</description>
<required>true</required>
</parameter>
</config-description>
<config-description uri="thing-type:netatmo:plug">
<parameter name="id" type="text" pattern="([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})">
<label>Equipment ID</label>
<description>Id of the Device (mac address)</description>
<required>true</required>
</parameter>
</config-description>
<config-description uri="thing-type:netatmo:module">
<parameter name="id" type="text" pattern="([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})">
<label>Module ID</label>
<description>Id of the Module</description>
<required>true</required>
</parameter>
<parameter name="parentId" type="text" pattern="([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})">
<label>Device ID</label>
<description>Id of the main device</description>
<required>true</required>
</parameter>
</config-description>
<config-description uri="thing-type:netatmo:natherm1">
<parameter name="id" type="text" pattern="([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})">
<label>Module ID</label>
<description>Id of the Module</description>
<required>true</required>
</parameter>
<parameter name="parentId" type="text" pattern="([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})">
<label>Device ID</label>
<description>Id of the main device</description>
<required>true</required>
</parameter>
<parameter name="setpointDefaultDuration" type="integer" required="false">
<label>Setpoint Duration</label>
<description>Default duration of thermostat change when force to max or manual.</description>
<default>60</default>
<advanced>true</advanced>
</parameter>
</config-description>
<config-description uri="thing-type:netatmo:welcomehome">
<parameter name="id" type="text" required="true">
<label>Home ID</label>
<description>UUID of the home</description>
</parameter>
<parameter name="refreshInterval" type="integer" min="2000" unit="ms" required="false">
<label>Refresh Interval</label>
<description>The refresh interval to poll Netatmo API (in ms).</description>
<default>300000</default>
<advanced>true</advanced>
</parameter>
</config-description>
<config-description uri="thing-type:netatmo:camera">
<parameter name="id" type="text" pattern="([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})">
<label>Camera ID</label>
<description>Camera MAC Address</description>
<required>true</required>
</parameter>
<parameter name="parentId" type="text">
<label>Home ID</label>
<description>UUID of the home hosting the camera</description>
<required>true</required>
</parameter>
</config-description>
<config-description uri="thing-type:netatmo:nawelcomeperson">
<parameter name="id" type="text">
<label>Person ID</label>
<description>UUID of the Person</description>
<required>true</required>
</parameter>
<parameter name="parentId" type="text">
<label>Home ID</label>
<description>UUID of the home</description>
<required>true</required>
</parameter>
</config-description>
</config-description:config-descriptions>

View File

@@ -0,0 +1,334 @@
# binding
binding.netatmo.name = Netatmo Binding
binding.netatmo.description = Dieses Binding integriert die smarte Wetterstation, die Zusatzmodule wie Regenmesser und Windmesser, als auch das smarte Thermostat und den Healthy Home Coach.
# bridge types
thing-type.netatmo.netatmoapi.label = Netatmo API
thing-type.netatmo.netatmoapi.description = Netatmo API
# bridge types config
thing-type.config.netatmo.bridge.clientId.label = Client id
thing-type.config.netatmo.bridge.clientId.description = Client id der APP, die unter https://dev.netatmo.com/myaccount/createanapp erstellt wurde.
thing-type.config.netatmo.bridge.clientSecret.label = Client secret
thing-type.config.netatmo.bridge.clientSecret.description = Client secret der APP, die unter https://dev.netatmo.com/myaccount/createanapp erstellt wurde.
thing-type.config.netatmo.bridge.username.label = Benutzer
thing-type.config.netatmo.bridge.username.description = Benutzer zur Authentifizierung an der Netatmo API.
thing-type.config.netatmo.bridge.password.label = Passwort
thing-type.config.netatmo.bridge.password.description = Passwort zur Authentifizierung an der Netatmo API.
thing-type.config.netatmo.bridge.readStation.label = Wetterstation API
thing-type.config.netatmo.bridge.readStation.description = Aktiviert den Zugriff auf die Wetterstation API (lesend).
thing-type.config.netatmo.bridge.readHealthyHomeCoach.label = Healthy Home Coach API
thing-type.config.netatmo.bridge.readHealthyHomeCoach.description = Aktiviert den Zugriff auf die Healthy Home Coach API (lesend).
thing-type.config.netatmo.bridge.readThermostat.label = Thermostat API
thing-type.config.netatmo.bridge.readThermostat.description = Aktiviert den Zugriff auf die Thermostat API (lesend und schreibend).
#thing-type.config.netatmo.bridge.readWelcome.label = Access Welcome camera
#thing-type.config.netatmo.bridge.readWelcome.description = Read and Access Welcome camera's data.
#thing-type.config.netatmo.bridge.readPresence.label = Access Presence camera
#thing-type.config.netatmo.bridge.readPresence.description = Read and Access Presence camera's data.
#thing-type.config.netatmo.bridge.webHookUrl.label = Webhook address
#thing-type.config.netatmo.bridge.webHookUrl.description = Protocol, public IP and port to access OH2 server from Internet.
#thing-type.config.netatmo.bridge.reconnectInterval.label = Reconnect Interval
#thing-type.config.netatmo.bridge.reconnectInterval.description = The reconnection interval to Netatmo API (in s).
# thing types
thing-type.netatmo.NAMain.label = Haupt-Indoor-Modul
thing-type.netatmo.NAMain.description = Das Haupt-Indoor-Modul liefert Daten wie z.B. Temperatur, Luftdruck, Luftfeuchtigkeit, CO2-Gehalt und Lautstärke.
thing-type.netatmo.NAModule1.label = Outdoor-Modul
thing-type.netatmo.NAModule1.description = Das Outdoor-Modul liefert Daten wie z.B. Temperatur und Luftdruck.
thing-type.netatmo.NAModule2.label = Windmesser
thing-type.netatmo.NAModule2.description = Der Windmesser liefert Daten wie z.B. Windrichtung und Windstärke.
thing-type.netatmo.NAModule3.label = Regenmesser
thing-type.netatmo.NAModule3.description = Der Regenmesser liefert Daten wie z.B. Echtzeit-Messungen zur Intensität des Regens.
thing-type.netatmo.NAModule4.label = Zusatz-Indoor-Modul
thing-type.netatmo.NAModule4.description = Das Zusatz-Indoor-Modul liefert Daten wie z.B. Temperatur, Luftdruck, Luftfeuchtigkeit und CO2-Gehalt.
thing-type.netatmo.NAPlug.label = Thermostat Relais
thing-type.netatmo.NAPlug.description = Das Thermostat Relais liefert Daten des Heizkessels.
thing-type.netatmo.NATherm1.label = Heizkörperthermostat
thing-type.netatmo.NATherm1.description = Das Heizkörperthermostat dient zur Steuerung von Heizkörpern und liefert Daten wie z.B. Temperatur.
thing-type.netatmo.NHC.label = Healthy Home Coach
thing-type.netatmo.NHC.description = Der Healthy Home Coach liefert Daten wie z.B. Temperatur, Luftdruck, Luftfeuchtigkeit, CO2-Gehalt und Lautstärke.
#thing-type.netatmo.NAWelcomeHome.label = Welcome Home
#thing-type.netatmo.NAWelcomeHome.description = This represents a home hosting a Welcome Camera.
#thing-type.netatmo.NACamera.label = Welcome Camera
#thing-type.netatmo.NACamera.description = This represents a welcome camera at home.
#thing-type.netatmo.NAWelcomePerson.label = Welcome Person
#thing-type.netatmo.NAWelcomePerson.description = This represents a person at home.
# thing types config
thing-type.config.netatmo.station.id.label = MAC-Adresse Modul
thing-type.config.netatmo.station.id.description = MAC-Adresse des Haupt-Indoor-Moduls.
thing-type.config.netatmo.module.id.label = MAC-Adresse Zusatzmodule
thing-type.config.netatmo.module.id.description = MAC-Adresse des Zusatzmoduls.
thing-type.config.netatmo.module.parentId.label = MAC-Adresse Haupt-Indoor-Modul
thing-type.config.netatmo.module.parentId.description = MAC-Adresse des Haupt-Indoor-Moduls.
#thing-type.config.netatmo.plug.id.label = Equipment ID
#thing-type.config.netatmo.plug.id.description = Id of the Device (mac address).
#thing-type.config.netatmo.natherm1.id.label = Module ID
#thing-type.config.netatmo.natherm1.id.description = Id of the Module.
#thing-type.config.netatmo.natherm1.parentId.label = Device ID
#thing-type.config.netatmo.natherm1.parentId.description = Id of the main device.
#thing-type.config.netatmo.natherm1.setpointDefaultDuration.label = Setpoint duration
#thing-type.config.netatmo.natherm1.setpointDefaultDuration.description = Default duration of thermostat change when force to max or manual.
#thing-type.config.netatmo.welcomehome.id.label = Home ID
#thing-type.config.netatmo.welcomehome.id.description = UUID of the home
#thing-type.config.netatmo.welcomehome.refreshInterval.label = Refresh Interval
#thing-type.config.netatmo.welcomehome.refreshInterval.description = The refresh interval to poll Netatmo API (in ms).
#thing-type.config.netatmo.camera.id.label = Camera ID
#thing-type.config.netatmo.camera.id.description = Camera MAC Address
#thing-type.config.netatmo.camera.parentId.label = Home ID
#thing-type.config.netatmo.camera.parentId.description = UUID of the home hosting the camera
#thing-type.config.netatmo.nawelcomeperson.id.label = Person ID
#thing-type.config.netatmo.nawelcomeperson.id.description = UUID of the Person
#thing-type.config.netatmo.nawelcomeperson.parentId.label = Home ID
#thing-type.config.netatmo.nawelcomeperson.parentId.description = UUID of the home
# channel types
channel-type.netatmo.co2.label = CO2-Gehalt
channel-type.netatmo.co2.description = Zeigt den aktuellen CO2-Gehalt an.
channel-type.netatmo.temperature.label = Temperatur
channel-type.netatmo.temperature.description = Zeigt die aktuelle Temperatur an.
channel-type.netatmo.minTemp.label = Min. Temperatur
channel-type.netatmo.minTemp.description = Zeigt die minimale Temperatur des aktuellen Tages an.
channel-type.netatmo.maxTemp.label = Max. Temperatur
channel-type.netatmo.maxTemp.description = Zeigt die maximale Temperatur des aktuellen Tages an.
channel-type.netatmo.dateMinTemp.label = Zeitpunkt Min. Temperatur
channel-type.netatmo.dateMinTemp.description = Zeigt den Zeitpunkt an, an dem die minimale Temperatur gemessen wurde.
channel-type.netatmo.dateMaxTemp.label = Zeitpunkt Max. Temperatur
channel-type.netatmo.dateMaxTemp.description = Zeigt den Zeitpunkt an, an dem die maximale Temperatur gemessen wurde.
channel-type.netatmo.temperatureTrend.label = Temperaturtrend
channel-type.netatmo.temperatureTrend.description = Zeigt den Temperaturtrend (z.B. "Steigend", "Stabil" oder "Fallend") an.
channel-type.netatmo.temperatureTrend.state.option.up = Steigend
channel-type.netatmo.temperatureTrend.state.option.stable = Stabil
channel-type.netatmo.temperatureTrend.state.option.down = Fallend
channel-type.netatmo.humidity.label = Luftfeuchtigkeit
channel-type.netatmo.humidity.description = Zeigt die aktuelle Luftfeuchtigkeit an.
channel-type.netatmo.humidex.label = Gef. Temperatur (Humidex)
channel-type.netatmo.humidex.description = Zeigt die gefühlte Temperatur an. Basierend auf dem Humidex.
channel-type.netatmo.heatIndex.label = Gef. Temperatur (Hitzeindex)
channel-type.netatmo.heatIndex.description = Zeigt die gefühlte Temperatur an. Basierend auf dem Hitzeindex.
channel-type.netatmo.dewPoint.label = Taupunkt
channel-type.netatmo.dewPoint.description = Zeigt den Taupunkt an.
channel-type.netatmo.dewPointDepression.label = Taupunktsdifferenz
channel-type.netatmo.dewPointDepression.description = Zeigt die Taupunktsdifferenz an.
channel-type.netatmo.noise.label = Sonometer
channel-type.netatmo.noise.description = Zeigt die aktuelle Lautstärke an.
channel-type.netatmo.pressure.label = Luftdruck
channel-type.netatmo.pressure.description = Zeigt den aktuellen Luftdruck an.
channel-type.netatmo.pressureTrend.label = Luftdrucktrend
channel-type.netatmo.pressureTrend.description = Zeigt den Luftdrucktrend an (z.B. "Steigend", "Stabil" oder "Fallend").
channel-type.netatmo.pressureTrend.state.option.up = Steigend
channel-type.netatmo.pressureTrend.state.option.stable = Stabil
channel-type.netatmo.pressureTrend.state.option.down = Fallend
channel-type.netatmo.absolutePressure.label = Absoluter Luftdruck
channel-type.netatmo.absolutePressure.description = Zeigt den absoluten Luftdruck an.
channel-type.netatmo.lastStatusStore.label = Letzte Übertragung
channel-type.netatmo.lastStatusStore.description = Zeigt den Zeitpunkt der letzten Datenübertragung an.
channel-type.netatmo.timeUtc.label = Letzte Messung
channel-type.netatmo.timeUtc.description = Zeigt den Zeitpunkt der letzten Datenmessung an.
channel-type.netatmo.lastMessage.label = Letzte Meldung
channel-type.netatmo.lastMessage.description = Zeigt den Zeitpunkt der letzten Meldung an.
channel-type.netatmo.location.label = Standort
channel-type.netatmo.location.description = Zeigt die geographischen Koordinaten (Breitengrad, Längengrad, Höhe über dem Meerespiegel) (in °N, °W, m) an.
channel-type.netatmo.rain.label = Niederschlag
channel-type.netatmo.rain.description = Zeigt den aktuellen Niederschlag an.
channel-type.netatmo.rain1.label = Kumulierter Niederschlag (1h)
channel-type.netatmo.rain1.description = Zeigt den kumulierten Niederschlag der letzten Stunde an.
channel-type.netatmo.rain24.label = Kumulierter Niederschlag (24h)
channel-type.netatmo.rain24.description = Zeigt den kumulierten Niederschlag der letzten 24h an.
channel-type.netatmo.WindAngle.label = Windrichtung
channel-type.netatmo.WindAngle.description = Zeigt die aktuelle Windrichtung an.
channel-type.netatmo.WindStrength.label = Windgeschwindigkeit
channel-type.netatmo.WindStrength.description = Zeigt die aktuelle Windgeschwindigkeit an.
channel-type.netatmo.GustAngle.label = Böen Richtung
channel-type.netatmo.GustAngle.description = Zeigt die aktuelle Böen Richtung an.
channel-type.netatmo.GustStrength.label = Böen Geschwindigkeit
channel-type.netatmo.GustStrength.description = Zeigt die aktuelle Böen Geschwindigkeit an.
channel-type.netatmo.healthindex.label = Health Index
channel-type.netatmo.healthindex.description = Zeigt den Health Index (z.B. "Gesund", "Gut", "Angemessen", "Schlecht" oder "Ungesund") an.
channel-type.netatmo.healthindex.state.option.healthy = Gesund
channel-type.netatmo.healthindex.state.option.fine = Gut
channel-type.netatmo.healthindex.state.option.fair = Angemessen
channel-type.netatmo.healthindex.state.option.poor = Schlecht
channel-type.netatmo.healthindex.state.option.unhealthy = Ungesund
#channel-type.netatmo.connectedBoiler.label = Plug Connected Boiler
#channel-type.netatmo.connectedBoiler.description =
#channel-type.netatmo.lastPlugSeen.label = Last Plug Seen
#channel-type.netatmo.lastPlugSeen.description = Last Plug Seen
#channel-type.netatmo.lastBilan.label = Available Bilan
#channel-type.netatmo.lastBilan.description = Month of the last available thermostat bilan
#channel-type.netatmo.setpointTemp.label = Setpoint
#channel-type.netatmo.setpointTemp.description = Thermostat temperature setpoint
#channel-type.netatmo.setpointMode.label = Setpoint Mode
#channel-type.netatmo.setpointMode.description = Chosen setpoint_mode (program, away, hg, manual, off, max)
#channel-type.netatmo.setpointMode.state.option.program = Following a weekly schedule
#channel-type.netatmo.setpointMode.state.option.away = Applying the -away- temperature as defined by the user
#channel-type.netatmo.setpointMode.state.option.hg = Frost-guard
#channel-type.netatmo.setpointMode.state.option.manual = Applying a manually set temperature setpoint
#channel-type.netatmo.setpointMode.state.option.off = Currently off
#channel-type.netatmo.setpointMode.state.option.max = Heating continuously
#channel-type.netatmo.ThermRelayCmd.label = Heating status
#channel-type.netatmo.ThermRelayCmd.description = Indicates whether the furnace is heating or not
#channel-type.netatmo.ThermOrientation.label = Orientation
#channel-type.netatmo.ThermOrientation.description = Physical orientation of the thermostat module
#channel-type.netatmo.setpointEndTime.label = Setpoint end
#channel-type.netatmo.setpointEndTime.description = Thermostat goes back to schedule after that timestamp.
#channel-type.netatmo.homecity.label = City
#channel-type.netatmo.homecity.description = City of the home
#channel-type.netatmo.homecountry.label = Country
#channel-type.netatmo.homecountry.description = Country of the home
#channel-type.netatmo.hometimezone.label = Timezone
#channel-type.netatmo.hometimezone.description = Timezone of the home
#channel-type.netatmo.homepersoncount.label = Person counter
#channel-type.netatmo.homepersoncount.description = Total number of Persons that are at home
#channel-type.netatmo.homeunknowncount.label = Unknown Person counter
#channel-type.netatmo.homeunknowncount.description = Count how many Unknown Persons are at home
#channel-type.netatmo.type.label = Type
#channel-type.netatmo.type.description = Type of event
#channel-type.netatmo.time.label = Time
#channel-type.netatmo.time.description = Time of occurrence of event
#channel-type.netatmo.camera_id.label = Camera ID
#channel-type.netatmo.camera_id.description = Camera that detected the event
#channel-type.netatmo.person_id.label = Person ID
#channel-type.netatmo.person_id.description = Id of the person the event is about (if any)
#channel-type.netatmo.snapshot_url.label = Snapshot URL
#channel-type.netatmo.snapshot_url.description = Url of the event snapshot
#channel-type.netatmo.snapshot.label = Event Snapshot
#channel-type.netatmo.snapshot.description = Event Snapshot
#channel-type.netatmo.video_url.label = Video URL
#channel-type.netatmo.video_url.description = URL of the event video
#channel-type.netatmo.video_status.label = Video status
#channel-type.netatmo.video_status.description = Status of the video (recording, deleted or available)
#channel-type.netatmo.is_arrival.label = Is arrival
#channel-type.netatmo.is_arrival.description = If person was considered "away" before being seen during this event
#channel-type.netatmo.message.label = Message
#channel-type.netatmo.message.description = Message sent by Netatmo corresponding to given event
#channel-type.netatmo.sub_type.label = Sub Type
#channel-type.netatmo.sub_type.description = Sub-type of SD and Alim events
#channel-type.netatmo.status.label = State
#channel-type.netatmo.status.description = State of the camera
#channel-type.netatmo.sd_status.label = SD State
#channel-type.netatmo.sd_status.description = State of the SD card
#channel-type.netatmo.alim_status.label = Alim State
#channel-type.netatmo.alim_status.description = State of the power connector
#channel-type.netatmo.is_locale.label = Is local
#channel-type.netatmo.is_locale.description = Indicates whether the camera is on the same network than the openHab Netatmo Binding
#channel-type.netatmo.live_picture_url.label = Live snapshot URL
#channel-type.netatmo.live_picture_url.description = Url of the live snapshot for this camera
#channel-type.netatmo.live_picture.label = Live Snapshot
#channel-type.netatmo.live_picture.description = Camera Live Snapshot
#channel-type.netatmo.live_stream_url.label = Live stream URL
#channel-type.netatmo.live_stream_url.description = Url of the live stream for this camera
#channel-type.netatmo.last_seen.label = Last seen
#channel-type.netatmo.last_seen.description = Time when this person was last seen
#channel-type.netatmo.person_athome.label = At home
#channel-type.netatmo.person_athome.description = Indicates if this person is known to be at home or not
#channel-type.netatmo.person_eventmsg.label = Last Event message
#channel-type.netatmo.person_eventmsg.description = Last Event message from this person
#channel-type.netatmo.person_eventtime.label = Last Event time
#channel-type.netatmo.person_eventtime.description = Last Event message time for this person
#channel-type.netatmo.person_avatar_url.label = Avatar URL
#channel-type.netatmo.person_avatar_url.description = URL for the avatar of this person
#channel-type.netatmo.person_avatar.label = Avatar
#channel-type.netatmo.person_avatar.description = Avatar of this person
#channel-type.netatmo.person_event.label = Last Event Picture
#channel-type.netatmo.person_event.description = Picture of the last event for this person
#channel-type.netatmo.person_event_url.label = Last event URL
#channel-type.netatmo.person_event_url.description = URL for the picture of the last event for this person

View File

@@ -0,0 +1,378 @@
# binding
binding.netatmo.name = Extension Netatmo
binding.netatmo.description = L'extension Netatmo intègre le thermostat et la station météo Netatmo ainsi que les modules additionels.
# bridge types
thing-type.netatmo.netatmoapi.label = API Netatmo
thing-type.netatmo.netatmoapi.description = Cet élément représente la passerelle avec l'API Netatmo.
# bridge types configuration
thing-type.config.netatmo.bridge.clientId.label = ID client
thing-type.config.netatmo.bridge.clientId.description = ID clent fourni par l'application que vous avez créée sur http://dev.netatmo.com/createapp
thing-type.config.netatmo.bridge.clientSecret.label = Secret client
thing-type.config.netatmo.bridge.clientSecret.description = Secret client fourni par l'application que vous avez créée.
thing-type.config.netatmo.bridge.username.label = Nom d'utilisateur
thing-type.config.netatmo.bridge.username.description = Votre nom d'utilisateur (e-mail) pour l'API Netatmo.
thing-type.config.netatmo.bridge.password.label = Mot de passe
thing-type.config.netatmo.bridge.password.description = Votre mot de passe pour l'API Netatmo.
thing-type.config.netatmo.bridge.readStation.label = Accès à la station météo
thing-type.config.netatmo.bridge.readStation.description = Accède ou non aux données de la station météo.
thing-type.config.netatmo.bridge.readHealthyHomeCoach.label = Accès au Healthy Home Coach
thing-type.config.netatmo.bridge.readHealthyHomeCoach.description = Accède ou non aux données du Healthy Home Coach.
thing-type.config.netatmo.bridge.readThermostat.label = Accès au thermostat
thing-type.config.netatmo.bridge.readThermostat.description = Accède ou non aux données du thermostat.
thing-type.config.netatmo.bridge.readWelcome.label = Accès à la Camera Welcome
thing-type.config.netatmo.bridge.readWelcome.description = Accède ou non aux données de la caméra Welcome.
thing-type.config.netatmo.bridge.readPresence.label = Accès à la Camera Presence
thing-type.config.netatmo.bridge.readPresence.description = Accède ou non aux données de la caméra Presence.
thing-type.config.netatmo.bridge.webHookUrl.label = Adresse Webhook
thing-type.config.netatmo.bridge.webHookUrl.description = Protocole, IP publique et port pour l'accès au sereveur OH2 depuis l'Internet.
thing-type.config.netatmo.bridge.reconnectInterval.label = Intervalle de reconnection
thing-type.config.netatmo.bridge.reconnectInterval.description = L'intervalle de reconnection à l'API Netatmo (en s).
# thing types
thing-type.netatmo.NAMain.label = Station intérieure principale
thing-type.netatmo.NAMain.description = Module capable de mesurer la température, l'humidité, la pression, la qualité de l'air et le niveau sonore.
thing-type.netatmo.NAModule1.label = Module Extérieur
thing-type.netatmo.NAModule1.description = Module extérieur capable de mesurer la température et l'humidité.
thing-type.netatmo.NAModule2.label = Anémomètre
thing-type.netatmo.NAModule2.description = Module extérieur dédié au vent capable de mesurer sa direction et sa force.
thing-type.netatmo.NAModule3.label = Capteur de Pluie
thing-type.netatmo.NAModule3.description = Module extérieur dédié à la mesure du niveau de précipitations.
thing-type.netatmo.NAModule4.label = Module Additionel
thing-type.netatmo.NAModule4.description = Module intérieur supplémentaire capable de mesurer la température, l'humidité et le niveau de CO2.
thing-type.netatmo.NAPlug.label = Relais Thermostat
thing-type.netatmo.NAPlug.description = Cet élément représente le relais communiquant avec le thermostat.
thing-type.netatmo.NATherm1.label = Module Thermostat
thing-type.netatmo.NATherm1.description = Cet élément représente le module thermostat permettant de règler la température.
thing-type.netatmo.NHC.label = Healthy Home Coach
thing-type.netatmo.NHC.description = Cet élément représente le module Healthy Home Coach capable de mesurer le niveau de confort dans la maison.
thing-type.netatmo.NAWelcomeHome.label = Maison
thing-type.netatmo.NAWelcomeHome.description = Cet élément représente une maison hébergeant une ou plusieurs caméras Netatmo.
thing-type.netatmo.NACamera.label = Caméra Welcome
thing-type.netatmo.NACamera.description = Cet élément représente une caméra Welcome de la maison.
thing-type.netatmo.NAWelcomePerson.label = Personne
thing-type.netatmo.NAWelcomePerson.description = Cet élément représente une personne de la maison.
# thing type configuration
thing-type.config.netatmo.station.id.label = ID équipement
thing-type.config.netatmo.station.id.description = ID de l'équipement (adresse MAC)
thing-type.config.netatmo.module.id.label = ID module
thing-type.config.netatmo.module.id.description = ID du module
thing-type.config.netatmo.module.parentId.label = ID équipement principal
thing-type.config.netatmo.module.parentId.description = ID de l'équipement principal
thing-type.config.netatmo.plug.id.label = ID équipement
thing-type.config.netatmo.plug.id.description = ID de l'équipement (adresse MAC)
thing-type.config.netatmo.natherm1.id.label = ID module
thing-type.config.netatmo.natherm1.id.description = ID du module
thing-type.config.netatmo.natherm1.parentId.label = ID équipement principal
thing-type.config.netatmo.natherm1.parentId.description = ID de l'équipement principal
thing-type.config.netatmo.natherm1.setpointDefaultDuration.label = Durée de consigne
thing-type.config.netatmo.natherm1.setpointDefaultDuration.description = Durée par défaut de consigne hors planning
thing-type.config.netatmo.welcomehome.id.label = ID maison
thing-type.config.netatmo.welcomehome.id.description = ID de la maison
thing-type.config.netatmo.welcomehome.refreshInterval.label = Fréquence de rafraîchissement
thing-type.config.netatmo.welcomehome.refreshInterval.description = La fréquence d'interrogation de l'API Netatmo (en ms)
thing-type.config.netatmo.camera.id.label = ID caméra
thing-type.config.netatmo.camera.id.description = ID de la caméra (adresse MAC)
thing-type.config.netatmo.camera.parentId.label = ID maison
thing-type.config.netatmo.camera.parentId.description = ID de la maison hébergeant la caméra
thing-type.config.netatmo.nawelcomeperson.id.label = ID personne
thing-type.config.netatmo.nawelcomeperson.id.description = ID de la personne
thing-type.config.netatmo.nawelcomeperson.parentId.label = ID maison
thing-type.config.netatmo.nawelcomeperson.parentId.description = ID de la maison
# channel types
channel-type.netatmo.co2.label = CO2
channel-type.netatmo.co2.description = Mesure de la qualité de l'air
channel-type.netatmo.temperature.label = Température
channel-type.netatmo.temperature.description = Mesure de la tempérture
channel-type.netatmo.temperatureTrend.label = Tendance température
channel-type.netatmo.temperatureTrend.description = Mesure de la tendance d'évolution de la tempérture
channel-type.netatmo.temperatureTrend.state.option.up = A la hausse
channel-type.netatmo.temperatureTrend.state.option.stable = Stable
channel-type.netatmo.temperatureTrend.state.option.down = A la baisse
channel-type.netatmo.noise.label = Niveau sonore
channel-type.netatmo.noise.description = Mesure du niveau sonore de la pièce
channel-type.netatmo.pressure.label = Pression atmosphérique
channel-type.netatmo.pressure.description = Mesure de la pression atmosphérique
channel-type.netatmo.pressureTrend.label = Tendance pression atmosphérique
channel-type.netatmo.pressureTrend.description = Mesure de la tendance d'évolution de la pression atmosphérique sur les dernières 12 heures
channel-type.netatmo.pressureTrend.state.option.up = A la hausse
channel-type.netatmo.pressureTrend.state.option.stable = Stable
channel-type.netatmo.pressureTrend.state.option.down = A la baisse
channel-type.netatmo.absolutePressure.label = Pression atmosphérique absolue
channel-type.netatmo.absolutePressure.description = Mesure de la pression atmosphérique absolue
channel-type.netatmo.timeUtc.label = Horodatage des mesures
channel-type.netatmo.timeUtc.description = Date/Heure du dernier relevé de mesures
channel-type.netatmo.humidity.label = Humidité
channel-type.netatmo.humidity.description = Mesure du niveau d'hygrométrie
channel-type.netatmo.humidex.label = Humidex
channel-type.netatmo.humidex.description = Indice humidex calculé
channel-type.netatmo.heatIndex.label = Indice de chaleur
channel-type.netatmo.heatIndex.description = Indice de chaleur calculé
channel-type.netatmo.dewPoint.label = Point de rosée
channel-type.netatmo.dewPoint.description = Température du point de rosée
channel-type.netatmo.dewPointDepression.label = Dépression du point de rosée
channel-type.netatmo.dewPointDepression.description = Ecart entre la température actuelle et le point de rosée
channel-type.netatmo.minTemp.label = Température min
channel-type.netatmo.minTemp.description = Température minimale de la journée en cours
channel-type.netatmo.maxTemp.label = Température max
channel-type.netatmo.maxTemp.description = Température maximale de la journée en cours
channel-type.netatmo.dateMinTemp.label = Horodatage température min
channel-type.netatmo.dateMinTemp.description = Date/Heure de relevé de la température minimale pour la journée en cours
channel-type.netatmo.dateMaxTemp.label = Horodatage température max
channel-type.netatmo.dateMaxTemp.description = Date/Heure de relevé de la température maximale pour la journée en cours
channel-type.netatmo.location.label = Localisation
channel-type.netatmo.location.description = Localisation de la station Netatmo
channel-type.netatmo.rain.label = Précipitations
channel-type.netatmo.rain.description = Volume de précipitations relevé
channel-type.netatmo.rain1.label = Précipitations 1h
channel-type.netatmo.rain1.description = Volume de précipitations relevé durant la dernière heure
channel-type.netatmo.rain24.label = Précipitations 24h
channel-type.netatmo.rain24.description = Volume de précipitations relevé durant la dernière journée
channel-type.netatmo.WindAngle.label = Direction du vent
channel-type.netatmo.WindAngle.description = Direction moyenne du vent sur les 5 dernières minutes
channel-type.netatmo.WindStrength.label = Force du vent
channel-type.netatmo.WindStrength.description = Vitesse moyenne du vent sur les 5 dernières minutes
channel-type.netatmo.GustAngle.label = Direction rafale de vent
channel-type.netatmo.GustAngle.description = Direction moyenne des rafales de vent sur les 5 dernières minutes
channel-type.netatmo.GustStrength.label = Force rafale de vent
channel-type.netatmo.GustStrength.description = Vitesse moyenne des rafales de vent sur les 5 dernières minutes
channel-type.netatmo.lastStatusStore.label = Dernière demande d'état
channel-type.netatmo.lastStatusStore.description = Date/Heure de la dernière demande d'état
channel-type.netatmo.lastMessage.label = Horodatage dernier message
channel-type.netatmo.lastMessage.description = Date/Heure du dernier message émis par le module
channel-type.netatmo.connectedBoiler.label = Relais connecté
channel-type.netatmo.connectedBoiler.description = Indique si le relais est connecté ou non à une chaudière
channel-type.netatmo.lastPlugSeen.label = Horodatage visibilité du relais
channel-type.netatmo.lastPlugSeen.description = Date/Heure de dernière visibilité du module relais par le thermostat
channel-type.netatmo.lastBilan.label = Bilan Economies d'Energie
channel-type.netatmo.lastBilan.description = Mois du dernier bilan d'économies d'énergie disponible
channel-type.netatmo.setpointTemp.label = Température de consigne
channel-type.netatmo.setpointTemp.description = Température de consigne sélectionnée sur le thermostat
channel-type.netatmo.setpointMode.label = Mode de consigne
channel-type.netatmo.setpointMode.description = Mode de consigne choisi sur le thermostat (planning hebdo, absence, hors-gel, manuel, arrêt, en permanence)
channel-type.netatmo.setpointMode.state.option.program = Suivi du planning hebdomadaire
channel-type.netatmo.setpointMode.state.option.away = Température d'absence
channel-type.netatmo.setpointMode.state.option.hg = Hors-gel
channel-type.netatmo.setpointMode.state.option.manual = Température de consigne manuelle
channel-type.netatmo.setpointMode.state.option.off = Arrêt
channel-type.netatmo.setpointMode.state.option.max = Chauffage en permanence
channel-type.netatmo.planning.label = Planning
channel-type.netatmo.planning.description = Planification des plages de chauffe utilisée en mode suivi du planning
channel-type.netatmo.ThermRelayCmd.label = Etat du chauffage
channel-type.netatmo.ThermRelayCmd.description = Indique si le chauffage est en marche ou pas
channel-type.netatmo.ThermOrientation.label = Orientation
channel-type.netatmo.ThermOrientation.description = Orientation physique du module thermostat
channel-type.netatmo.setpointEndTime.label = Heure fin de consigne
channel-type.netatmo.setpointEndTime.description = Heure de retour au planning de chauffe
channel-type.netatmo.healthindex.label = Indice de confort
channel-type.netatmo.healthindex.description = Indice de confort (sain, agréable, correct, mauvais, malsain)
channel-type.netatmo.healthindex.state.option.healthy = Sain
channel-type.netatmo.healthindex.state.option.fine = Agréable
channel-type.netatmo.healthindex.state.option.fair = Correct
channel-type.netatmo.healthindex.state.option.poor = Mauvais
channel-type.netatmo.healthindex.state.option.unhealthy = Malsain
channel-type.netatmo.homecity.label = Ville
channel-type.netatmo.homecity.description = Ville
channel-type.netatmo.homecountry.label = Pays
channel-type.netatmo.homecountry.description = Pays
channel-type.netatmo.hometimezone.label = Fuseau horaire
channel-type.netatmo.hometimezone.description = Fuseau horaire
channel-type.netatmo.homepersoncount.label = Compteur de personnes
channel-type.netatmo.homepersoncount.description = Nombre de personnes qui sont à la maison
channel-type.netatmo.homeunknowncount.label = Compteur de personnes inconnues
channel-type.netatmo.homeunknowncount.description = Nombre de personnes inconnues qui sont à la maison
channel-type.netatmo.type.label = Type de l'évènement
channel-type.netatmo.type.description = Type du dernier évènement
channel-type.netatmo.time.label = Horodatage de l'évènement
channel-type.netatmo.time.description = Date/Heure du dernier évènement
channel-type.netatmo.camera_id.label = ID caméra
channel-type.netatmo.camera_id.description = Caméra à l'origine du dernier évènement
channel-type.netatmo.person_id.label = ID personne
channel-type.netatmo.person_id.description = Id de la personne concernée par le dernier évènement
channel-type.netatmo.snapshot_url.label = URL image capturée
channel-type.netatmo.snapshot_url.description = Url de l'image capturée (quand une image est associée au dernier évènement)
channel-type.netatmo.snapshot.label = Image capturée
channel-type.netatmo.snapshot.description = Image capturée (quand une image est associée au dernier évènement)
channel-type.netatmo.video_url.label = URL vidéo capturée
channel-type.netatmo.video_url.description = Url de la vidéo capturée (quand une vidéo est associée au dernier évènement)
channel-type.netatmo.video_status.label = Etat de la vidéo
channel-type.netatmo.video_status.description = Etat de la vidéo associée au dernier évènement (recording, deleted or available)
channel-type.netatmo.is_arrival.label = Personne arrivant
channel-type.netatmo.is_arrival.description = Si cet évènement indique la détection d'une personne qui était considérée comme absente auparavant
channel-type.netatmo.message.label = Message de l'évènement
channel-type.netatmo.message.description = Message correspondant au dernier évènement
channel-type.netatmo.sub_type.label = Sous-type de l'évènement
channel-type.netatmo.sub_type.description = Sous-type du dernier évènement (disponible uniquement pour certains évènements)
channel-type.netatmo.status.label = Etat de la caméra
channel-type.netatmo.status.description = Etat de la caméra
channel-type.netatmo.sd_status.label = Etat de la carte SD
channel-type.netatmo.sd_status.description = Etat de la carte SD
channel-type.netatmo.alim_status.label = Etat de l'alimentation
channel-type.netatmo.alim_status.description = Etat de l'alimentation
channel-type.netatmo.is_locale.label = Caméra locale
channel-type.netatmo.is_locale.description = Indique si la caméra est dans le même réseau local que le logiciel
channel-type.netatmo.live_picture_url.label = URL image en direct
channel-type.netatmo.live_picture_url.description = Url de l'image en direct de la caméra
channel-type.netatmo.live_picture.label = Image en direct
channel-type.netatmo.live_picture.description = Image en direct de la caméra
channel-type.netatmo.live_stream_url.label = URL flux vidéo en direct
channel-type.netatmo.live_stream_url.description = Url du flux vidéo en direct de la caméra
channel-type.netatmo.last_seen.label = Date dernière détection
channel-type.netatmo.last_seen.description = Date où cette personne a été reconnue pour la dernière fois
channel-type.netatmo.person_athome.label = A la maison
channel-type.netatmo.person_athome.description = Indique si cette personne est connue comme étant ou non à la masioon
channel-type.netatmo.person_eventmsg.label = Dernier message
channel-type.netatmo.person_eventmsg.description = Dernier message relatif à cette personne
channel-type.netatmo.person_eventtime.label = Date dernier message
channel-type.netatmo.person_eventtime.description = Date du dernier message relatif à cette personne
channel-type.netatmo.person_avatar_url.label = URL avatar
channel-type.netatmo.person_avatar_url.description = Url de l'avatar de la personne
channel-type.netatmo.person_avatar.label = Avatar
channel-type.netatmo.person_avatar.description = Avatar de la personne
channel-type.netatmo.person_event.label = Dernière image
channel-type.netatmo.person_event.description = Image associée au dernier évènement relatif à cette personne
channel-type.netatmo.person_event_url.label = URL de la dernière image
channel-type.netatmo.person_event_url.description = URL de l'mage associée au dernier évènement relatif à cette personne
# Thing channels
thing-type.netatmo.NAMain.channel.WifiStatus.label = Niveau Wifi
thing-type.netatmo.NAMain.channel.WifiStatus.description = Indicateur de la qualité de signal Wifi
thing-type.netatmo.NAPlug.channel.WifiStatus.label = Niveau Wifi
thing-type.netatmo.NAPlug.channel.WifiStatus.description = Indicateur de la qualité de signal Wifi
thing-type.netatmo.NAModule1.channel.RfStatus.label = Niveau radio
thing-type.netatmo.NAModule1.channel.RfStatus.description = Indicateur de la qualité de signal radio
thing-type.netatmo.NAModule1.channel.BatteryVP.label = Niveau piles
thing-type.netatmo.NAModule1.channel.BatteryVP.description = Indicateur du niveau de piles
thing-type.netatmo.NAModule1.channel.LowBattery.label = Piles faibles
thing-type.netatmo.NAModule1.channel.LowBattery.description = Indicateur du niveau faible des piles
thing-type.netatmo.NAModule2.channel.RfStatus.label = Niveau radio
thing-type.netatmo.NAModule2.channel.RfStatus.description = Indicateur de la qualité de signal radio
thing-type.netatmo.NAModule2.channel.BatteryVP.label = Niveau piles
thing-type.netatmo.NAModule2.channel.BatteryVP.description = Indicateur du niveau de piles
thing-type.netatmo.NAModule2.channel.LowBattery.label = Piles faibles
thing-type.netatmo.NAModule2.channel.LowBattery.description = Indicateur du niveau faible des piles
thing-type.netatmo.NAModule3.channel.RfStatus.label = Niveau radio
thing-type.netatmo.NAModule3.channel.RfStatus.description = Indicateur de la qualité de signal radio
thing-type.netatmo.NAModule3.channel.BatteryVP.label = Niveau piles
thing-type.netatmo.NAModule3.channel.BatteryVP.description = Indicateur du niveau de piles
thing-type.netatmo.NAModule3.channel.LowBattery.label = Piles faibles
thing-type.netatmo.NAModule3.channel.LowBattery.description = Indicateur du niveau faible des piles
thing-type.netatmo.NAModule4.channel.RfStatus.label = Niveau radio
thing-type.netatmo.NAModule4.channel.RfStatus.description = Indicateur de la qualité de signal radio
thing-type.netatmo.NAModule4.channel.BatteryVP.label = Niveau piles
thing-type.netatmo.NAModule4.channel.BatteryVP.description = Indicateur du niveau de piles
thing-type.netatmo.NAModule4.channel.LowBattery.label = Piles faibles
thing-type.netatmo.NAModule4.channel.LowBattery.description = Indicateur du niveau faible des piles
thing-type.netatmo.NATherm1.channel.RfStatus.label = Niveau radio
thing-type.netatmo.NATherm1.channel.RfStatus.description = Indicateur de la qualité de signal radio
thing-type.netatmo.NATherm1.channel.BatteryVP.label = Niveau piles
thing-type.netatmo.NATherm1.channel.BatteryVP.description = Indicateur du niveau de piles
thing-type.netatmo.NATherm1.channel.LowBattery.label = Piles faibles
thing-type.netatmo.NATherm1.channel.LowBattery.description = Indicateur du niveau faible des piles

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="netatmo"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<!-- Netatmo API Bridge -->
<bridge-type id="netatmoapi">
<label>Netatmo API</label>
<description>This bridge represents the gateway to Netatmo API.</description>
<config-description-ref uri="thing-type:netatmo:bridge"/>
</bridge-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,116 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="netatmo"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="NACamera">
<supported-bridge-type-refs>
<bridge-type-ref id="netatmoapi"/>
</supported-bridge-type-refs>
<label>Welcome Camera</label>
<description>This represents a welcome camera at home</description>
<channels>
<channel id="welcomeCameraStatus" typeId="status"></channel>
<channel id="welcomeCameraSdStatus" typeId="sd_status"></channel>
<channel id="welcomeCameraAlimStatus" typeId="alim_status"></channel>
<channel id="welcomeCameraIsLocal" typeId="is_locale"></channel>
<channel id="welcomeCameraLivePicture" typeId="live_picture"></channel>
<channel id="welcomeCameraLivePictureUrl" typeId="live_picture_url"></channel>
<channel id="welcomeCameraLiveStreamUrl" typeId="live_stream_url"></channel>
<channel id="welcomeHomeEvent" typeId="homeEvent"></channel>
</channels>
<representation-property>id</representation-property>
<config-description-ref uri="thing-type:netatmo:camera"/>
</thing-type>
<thing-type id="NOC">
<supported-bridge-type-refs>
<bridge-type-ref id="netatmoapi"/>
</supported-bridge-type-refs>
<label>Presence Camera</label>
<description>This represents a presence camera at home</description>
<channels>
<channel id="cameraStatus" typeId="status"></channel>
<channel id="cameraSdStatus" typeId="sd_status"></channel>
<channel id="cameraAlimStatus" typeId="alim_status"></channel>
<channel id="cameraIsLocal" typeId="is_locale"></channel>
<channel id="cameraLivePicture" typeId="live_picture"></channel>
<channel id="cameraLivePictureUrl" typeId="live_picture_url"></channel>
<channel id="cameraLiveStreamUrl" typeId="live_stream_url"></channel>
<channel id="cameraFloodlightAutoMode" typeId="floodlightAutoMode"></channel>
<channel id="cameraFloodlight" typeId="floodlight"></channel>
</channels>
<representation-property>id</representation-property>
<config-description-ref uri="thing-type:netatmo:camera"/>
</thing-type>
<channel-type id="status">
<item-type>Switch</item-type>
<label>State</label>
<description>State of the camera</description>
</channel-type>
<channel-type id="sd_status">
<item-type>Switch</item-type>
<label>SD State</label>
<description>State of the SD card</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="alim_status">
<item-type>Switch</item-type>
<label>Alim State</label>
<description>State of the power connector</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="is_locale" advanced="true">
<item-type>Switch</item-type>
<label>Is Local</label>
<description>Only for scope access_camera. If Camera and application requesting the information are on the same
network (true/false)</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="live_picture_url" advanced="true">
<item-type>String</item-type>
<label>Live Snapshot URL</label>
<description>Url of the live snapshot for this camera (need scope access_camera)</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="live_picture">
<item-type>Image</item-type>
<label>Live Snapshot</label>
<description>Camera Live Snapshot</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="live_stream_url" advanced="true">
<item-type>String</item-type>
<label>Live Stream URL</label>
<description>Url of the live stream for this camera</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="floodlightAutoMode">
<item-type>Switch</item-type>
<label>Floodlight Auto-Mode</label>
<description>State of the floodlight auto-mode</description>
</channel-type>
<channel-type id="floodlight">
<item-type>Switch</item-type>
<label>Floodlight</label>
<description>State of the floodlight</description>
</channel-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,795 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="netatmo"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<channel-type id="lastStatusStore" advanced="true">
<item-type>DateTime</item-type>
<label>Last Status Store</label>
<description>Last Status Store</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="location">
<item-type>Location</item-type>
<label>Location</label>
<description>Location of the device</description>
<state readOnly="true" pattern="%2$s°N,%3$s°W, %1$s m"/>
</channel-type>
<channel-type id="temperature">
<item-type>Number:Temperature</item-type>
<label>Temperature</label>
<description>Current temperature</description>
<category>Temperature</category>
<state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="minTemp" advanced="true">
<item-type>Number:Temperature</item-type>
<label>Min Temp</label>
<description>Minimum Temperature on current day</description>
<category>Temperature</category>
<state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="minTempThisWeek" advanced="true">
<item-type>Number:Temperature</item-type>
<label>Min Temp This Week</label>
<description>Minimum Temperature this week</description>
<category>Temperature</category>
<state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="minTempThisMonth" advanced="true">
<item-type>Number:Temperature</item-type>
<label>Min Temp This Month</label>
<description>Minimum Temperature this month</description>
<category>Temperature</category>
<state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="maxTemp" advanced="true">
<item-type>Number:Temperature</item-type>
<label>Max Temp</label>
<description>Maximum Temperature on current day</description>
<category>Temperature</category>
<state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="maxTempThisWeek" advanced="true">
<item-type>Number:Temperature</item-type>
<label>Max Temp This Week</label>
<description>Maximum Temperature this week</description>
<category>Temperature</category>
<state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="maxTempThisMonth" advanced="true">
<item-type>Number:Temperature</item-type>
<label>Max Temp This Month</label>
<description>Maximum Temperature this month</description>
<category>Temperature</category>
<state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="temperatureTrend" advanced="true">
<item-type>String</item-type>
<label>Temp Trend</label>
<description>Temperature Evolution Trend</description>
<category>Temperature</category>
<state readOnly="true" pattern="%s">
<options>
<option value="up">up</option>
<option value="stable">stable</option>
<option value="down">down</option>
</options>
</state>
</channel-type>
<channel-type id="setpointTemp">
<item-type>Number:Temperature</item-type>
<label>Setpoint</label>
<description>Thermostat temperature setpoint</description>
<category>Temperature</category>
<state pattern="%.1f %unit%" readOnly="false"/>
</channel-type>
<channel-type id="setpointMode">
<item-type>String</item-type>
<label>Setpoint Mode</label>
<description>Chosen setpoint_mode (program, away, hg, manual, off, max)</description>
<state readOnly="false">
<options>
<option value="program">Following a weekly schedule</option>
<option value="away">Applying the -away- temperature as defined by the user</option>
<option value="hg">Frost-guard</option>
<option value="manual">Applying a manually set temperature setpoint</option>
<option value="off">Currently off</option>
<option value="max">Heating continuously</option>
</options>
</state>
</channel-type>
<channel-type id="ThermRelayCmd" advanced="false">
<item-type>Switch</item-type>
<label>Heating Status</label>
<description>Indicates whether the furnace is heating or not</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="ThermOrientation" advanced="true">
<item-type>Number</item-type>
<label>Orientation</label>
<description>Physical orientation of the thermostat module</description>
<state readOnly="true" pattern="%d"/>
</channel-type>
<channel-type id="timeUtc" advanced="true">
<item-type>DateTime</item-type>
<label>Measurement Time</label>
<description>Timestamp when data was measured</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="lastPlugSeen" advanced="true">
<item-type>DateTime</item-type>
<label>Last Plug Seen</label>
<description>Last Plug Seen</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="dateMinCo2" advanced="true">
<item-type>DateTime</item-type>
<label>Date Min CO2</label>
<description>Date when minimum CO2 was reached on current day</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="dateMinCo2ThisWeek" advanced="true">
<item-type>DateTime</item-type>
<label>Date Min CO2 This Week</label>
<description>Date when minimum CO2 was reached this week</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="dateMinCo2ThisMonth" advanced="true">
<item-type>DateTime</item-type>
<label>Date Min CO2 This Month</label>
<description>Date when minimum CO2 was reached this month</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="dateMaxCo2" advanced="true">
<item-type>DateTime</item-type>
<label>Date Max CO2</label>
<description>Date when maximum CO2 was reached on current day</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="dateMaxCo2ThisWeek" advanced="true">
<item-type>DateTime</item-type>
<label>Date Max CO2 This Week</label>
<description>Date when maximum CO2 was reached this week</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="dateMaxCo2ThisMonth" advanced="true">
<item-type>DateTime</item-type>
<label>Date Max CO2 This Month</label>
<description>Date when maximum CO2 was reached this month</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="dateMinTemp" advanced="true">
<item-type>DateTime</item-type>
<label>Date Min Temp</label>
<description>Date when minimum temperature was reached on current day</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="dateMinTempThisWeek" advanced="true">
<item-type>DateTime</item-type>
<label>Date Min Temp This Week</label>
<description>Date when minimum temperature was reached this week</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="dateMinTempThisMonth" advanced="true">
<item-type>DateTime</item-type>
<label>Date Min Temp This Month</label>
<description>Date when minimum temperature was reached this month</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="dateMaxTemp" advanced="true">
<item-type>DateTime</item-type>
<label>Date Max Temp</label>
<description>Date when maximum temperature was reached on current day</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="dateMaxTempThisWeek" advanced="true">
<item-type>DateTime</item-type>
<label>Date Max Temp This Week</label>
<description>Date when maximum temperature was reached this week</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="dateMaxTempThisMonth" advanced="true">
<item-type>DateTime</item-type>
<label>Date Max Temp This Month</label>
<description>Date when maximum temperature was reached this month</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="dateMinHumidity" advanced="true">
<item-type>DateTime</item-type>
<label>Date Min Humidity</label>
<description>Date when minimum humidity was reached on current day</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="dateMinHumidityThisWeek" advanced="true">
<item-type>DateTime</item-type>
<label>Date Min Humidity This Week</label>
<description>Date when minimum humidity was reached this week</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="dateMinHumidityThisMonth" advanced="true">
<item-type>DateTime</item-type>
<label>Date Min Humidity This Month</label>
<description>Date when minimum humidity was reached this month</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="dateMaxHumidity" advanced="true">
<item-type>DateTime</item-type>
<label>Date Max Humidity</label>
<description>Date when maximum humidity was reached on current day</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="dateMaxHumidityThisWeek" advanced="true">
<item-type>DateTime</item-type>
<label>Date Max Humidity This Week</label>
<description>Date when maximum humidity was reached this week</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="dateMaxHumidityThisMonth" advanced="true">
<item-type>DateTime</item-type>
<label>Date Max Humidity This Month</label>
<description>Date when maximum humidity was reached this month</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="dateMinNoise" advanced="true">
<item-type>DateTime</item-type>
<label>Date Min Noise</label>
<description>Date when minimum noise was reached on current day</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="dateMinNoiseThisWeek" advanced="true">
<item-type>DateTime</item-type>
<label>Date Min Noise This Week</label>
<description>Date when minimum noise was reached this week</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="dateMinNoiseThisMonth" advanced="true">
<item-type>DateTime</item-type>
<label>Date Min Noise This Month</label>
<description>Date when minimum noise was reached this month</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="dateMaxNoise" advanced="true">
<item-type>DateTime</item-type>
<label>Date Max Noise</label>
<description>Date when maximum noise was reached on current day</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="dateMaxNoiseThisWeek" advanced="true">
<item-type>DateTime</item-type>
<label>Date Max Noise This Week</label>
<description>Date when maximum noise was reached this week</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="dateMaxNoiseThisMonth" advanced="true">
<item-type>DateTime</item-type>
<label>Date Max Noise This Month</label>
<description>Date when maximum noise was reached this month</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="dateMinPressure" advanced="true">
<item-type>DateTime</item-type>
<label>Date Min Pressure</label>
<description>Date when minimum pressure was reached on current day</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="dateMinPressureThisWeek" advanced="true">
<item-type>DateTime</item-type>
<label>Date Min Pressure This Week</label>
<description>Date when minimum pressure was reached this week</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="dateMinPressureThisMonth" advanced="true">
<item-type>DateTime</item-type>
<label>Date Min Pressure This Month</label>
<description>Date when minimum pressure was reached this month</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="dateMaxPressure" advanced="true">
<item-type>DateTime</item-type>
<label>Date Max Pressure</label>
<description>Date when maximum pressure was reached on current day</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="dateMaxPressureThisWeek" advanced="true">
<item-type>DateTime</item-type>
<label>Date Max Pressure This Week</label>
<description>Date when maximum pressure was reached this week</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="dateMaxPressureThisMonth" advanced="true">
<item-type>DateTime</item-type>
<label>Date Max Pressure This Month</label>
<description>Date when maximum pressure was reached this month</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="lastBilan" advanced="true">
<item-type>DateTime</item-type>
<label>Available Bilan</label>
<description>Month of the last available thermostat bilan</description>
<state readOnly="true" pattern="%1$td.%1$tm.%1$tY"/>
</channel-type>
<channel-type id="connectedBoiler" advanced="true">
<item-type>Switch</item-type>
<label>Plug Connected Boiler</label>
<state readOnly="true"/>
</channel-type>
<channel-type id="lastMessage" advanced="true">
<item-type>DateTime</item-type>
<label>Last Message</label>
<description>Last Message emitted by the module</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="setpointEndTime" advanced="true">
<item-type>DateTime</item-type>
<label>Setpoint End</label>
<description>Thermostat goes back to schedule after that timestamp.</description>
<state readOnly="true" pattern="%1$td.%1$tm.%1$tY %1$tH:%1$tM"/>
</channel-type>
<channel-type id="lastThermSeen" advanced="true">
<item-type>DateTime</item-type>
<label>Last Therm Seen</label>
<description>Last Them Seen</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="co2">
<item-type>Number:Dimensionless</item-type>
<label>CO2</label>
<description>Air Quality</description>
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>
<channel-type id="minCo2" advanced="true">
<item-type>Number:Dimensionless</item-type>
<label>Min Co2</label>
<description>Minimum CO2 on current day</description>
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>
<channel-type id="minCo2ThisWeek" advanced="true">
<item-type>Number:Dimensionless</item-type>
<label>Min Co2 This Week</label>
<description>Minimum CO2 this week</description>
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>
<channel-type id="minCo2ThisMonth" advanced="true">
<item-type>Number:Dimensionless</item-type>
<label>Min Co2 This Month</label>
<description>Minimum CO2 this month</description>
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>
<channel-type id="maxCo2" advanced="true">
<item-type>Number:Dimensionless</item-type>
<label>Max Co2</label>
<description>Maximum CO2 on current day</description>
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>
<channel-type id="maxCo2ThisWeek" advanced="true">
<item-type>Number:Dimensionless</item-type>
<label>Max Co2 This Week</label>
<description>Maximum CO2 this week</description>
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>
<channel-type id="maxCo2ThisMonth" advanced="true">
<item-type>Number:Dimensionless</item-type>
<label>Max Co2 This Month</label>
<description>Maximum CO2 this month</description>
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>
<channel-type id="noise">
<item-type>Number:Dimensionless</item-type>
<label>Noise</label>
<description>Current Noise Level</description>
<category>Noise</category>
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>
<channel-type id="minNoise" advanced="true">
<item-type>Number:Dimensionless</item-type>
<label>Min Noise</label>
<description>Minimum Noise on current day</description>
<category>Noise</category>
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>
<channel-type id="minNoiseThisWeek" advanced="true">
<item-type>Number:Dimensionless</item-type>
<label>Min Noise This Week</label>
<description>Minimum Noise this week</description>
<category>Noise</category>
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>
<channel-type id="minNoiseThisMonth" advanced="true">
<item-type>Number:Dimensionless</item-type>
<label>Min Noise This Month</label>
<description>Minimum Noise this month</description>
<category>Noise</category>
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>
<channel-type id="maxNoise" advanced="true">
<item-type>Number:Dimensionless</item-type>
<label>Max Noise</label>
<description>Maximum Noise on current day</description>
<category>Noise</category>
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>
<channel-type id="maxNoiseThisWeek" advanced="true">
<item-type>Number:Dimensionless</item-type>
<label>Max Noise This Week</label>
<description>Maximum Noise this week</description>
<category>Noise</category>
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>
<channel-type id="maxNoiseThisMonth" advanced="true">
<item-type>Number:Dimensionless</item-type>
<label>Max Noise This Month</label>
<description>Maximum Noise this month</description>
<category>Noise</category>
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>
<channel-type id="healthindex">
<item-type>String</item-type>
<label>Health Index</label>
<description>Health Index (healthy, fine, fair, poor, unhealthy)</description>
<state readOnly="true" pattern="%s">
<options>
<option value="healthy">healthy</option>
<option value="fine">fine</option>
<option value="fair">fair</option>
<option value="poor">poor</option>
<option value="unhealthy">unhealthy</option>
</options>
</state>
</channel-type>
<channel-type id="pressure">
<item-type>Number:Pressure</item-type>
<label>Pressure</label>
<description>Current pressure</description>
<category>Pressure</category>
<state readOnly="true" pattern="%.3f %unit%"/>
</channel-type>
<channel-type id="minPressure" advanced="true">
<item-type>Number:Pressure</item-type>
<label>Min Pressure</label>
<description>Minimum Pressure on current day</description>
<category>Pressure</category>
<state readOnly="true" pattern="%.3f %unit%"/>
</channel-type>
<channel-type id="minPressureThisWeek" advanced="true">
<item-type>Number:Pressure</item-type>
<label>Min Pressure This Week</label>
<description>Minimum Pressure this week</description>
<category>Pressure</category>
<state readOnly="true" pattern="%.3f %unit%"/>
</channel-type>
<channel-type id="minPressureThisMonth" advanced="true">
<item-type>Number:Pressure</item-type>
<label>Min Pressure This Month</label>
<description>Minimum Pressure this month</description>
<category>Pressure</category>
<state readOnly="true" pattern="%.3f %unit%"/>
</channel-type>
<channel-type id="maxPressure" advanced="true">
<item-type>Number:Pressure</item-type>
<label>Max Pressure</label>
<description>Maximum Pressure on current day</description>
<category>Pressure</category>
<state readOnly="true" pattern="%.3f %unit%"/>
</channel-type>
<channel-type id="maxPressureThisWeek" advanced="true">
<item-type>Number:Pressure</item-type>
<label>Max Pressure This Week</label>
<description>Maximum Pressure this week</description>
<category>Pressure</category>
<state readOnly="true" pattern="%.3f %unit%"/>
</channel-type>
<channel-type id="maxPressureThisMonth" advanced="true">
<item-type>Number:Pressure</item-type>
<label>Max Pressure This Month</label>
<description>Maximum Pressure this month</description>
<category>Pressure</category>
<state readOnly="true" pattern="%.3f %unit%"/>
</channel-type>
<channel-type id="pressureTrend" advanced="true">
<item-type>String</item-type>
<label>Pressure Trend</label>
<description>Pressure evolution trend for last 12h (up, down, stable)</description>
<category>Pressure</category>
<state readOnly="true" pattern="%s">
<options>
<option value="up">up</option>
<option value="stable">stable</option>
<option value="down">down</option>
</options>
</state>
</channel-type>
<channel-type id="planning" advanced="false">
<item-type>String</item-type>
<label>Planning</label>
<description>Heat planning currently used</description>
<state pattern="%s"/>
</channel-type>
<channel-type id="absolutePressure" advanced="true">
<item-type>Number:Pressure</item-type>
<label>Abs Pressure</label>
<description>Absolute pressure</description>
<category>Pressure</category>
<state readOnly="true" pattern="%.3f %unit%"/>
</channel-type>
<channel-type id="humidity">
<item-type>Number:Dimensionless</item-type>
<label>Humidity</label>
<description>Current humidity</description>
<category>Humidity</category>
<state readOnly="true" min="0" max="100" pattern="%d %unit%"/>
</channel-type>
<channel-type id="minHumidity" advanced="true">
<item-type>Number:Dimensionless</item-type>
<label>Min Humidity</label>
<description>Minimum Humidity on current day</description>
<category>Humidity</category>
<state readOnly="true" min="0" max="100" pattern="%d %unit%"/>
</channel-type>
<channel-type id="minHumidityThisWeek" advanced="true">
<item-type>Number:Dimensionless</item-type>
<label>Min Humidity This Week</label>
<description>Minimum Humidity this week</description>
<category>Humidity</category>
<state readOnly="true" min="0" max="100" pattern="%d %unit%"/>
</channel-type>
<channel-type id="minHumidityThisMonth" advanced="true">
<item-type>Number:Dimensionless</item-type>
<label>Min Humidity This Month</label>
<description>Minimum Humidity this month</description>
<category>Humidity</category>
<state readOnly="true" min="0" max="100" pattern="%d %unit%"/>
</channel-type>
<channel-type id="maxHumidity" advanced="true">
<item-type>Number:Dimensionless</item-type>
<label>Max Humidity</label>
<description>Maximum Humidity on current day</description>
<category>Humidity</category>
<state readOnly="true" min="0" max="100" pattern="%d %unit%"/>
</channel-type>
<channel-type id="maxHumidityThisWeek" advanced="true">
<item-type>Number:Dimensionless</item-type>
<label>Max Humidity This Week</label>
<description>Minimum Humidity this week</description>
<category>Humidity</category>
<state readOnly="true" min="0" max="100" pattern="%d %unit%"/>
</channel-type>
<channel-type id="maxHumidityThisMonth" advanced="true">
<item-type>Number:Dimensionless</item-type>
<label>Max Humidity This Month</label>
<description>Maximum Humidity this month</description>
<category>Humidity</category>
<state readOnly="true" min="0" max="100" pattern="%d %unit%"/>
</channel-type>
<channel-type id="humidex">
<item-type>Number</item-type>
<label>Humidex</label>
<description>Computed Humidex index</description>
<category>Temperature</category>
<state readOnly="true" pattern="%.0f"/>
</channel-type>
<channel-type id="heatIndex">
<item-type>Number:Temperature</item-type>
<label>Heat Index</label>
<description>Computed Heat Index</description>
<category>Temperature</category>
<state readOnly="true" pattern="%.0f %unit%"/>
</channel-type>
<channel-type id="dewPoint" advanced="true">
<item-type>Number:Temperature</item-type>
<label>Dewpoint</label>
<description>Computed Dewpoint Temperature</description>
<category>Temperature</category>
<state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="dewPointDepression" advanced="true">
<item-type>Number:Temperature</item-type>
<label>Dewpoint Depression</label>
<description>Computed Dewpoint Depression</description>
<state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="rain">
<item-type>Number:Length</item-type>
<label>Rain</label>
<description>Quantity of water</description>
<category>Rain</category>
<state readOnly="true" pattern="%.2f %unit%"/>
</channel-type>
<channel-type id="rain1" advanced="true">
<item-type>Number:Length</item-type>
<label>Rain 1h</label>
<description>Quantity of water on last hour</description>
<category>Rain</category>
<state readOnly="true" pattern="%.2f %unit%"/>
</channel-type>
<channel-type id="rain24" advanced="true">
<item-type>Number:Length</item-type>
<label>Rain 24h</label>
<description>Quantity of water on last day</description>
<category>Rain</category>
<state readOnly="true" pattern="%.2f %unit%"/>
</channel-type>
<channel-type id="rainThisWeek" advanced="true">
<item-type>Number:Length</item-type>
<label>Rain This Week</label>
<description>Quantity of water this week</description>
<category>Rain</category>
<state readOnly="true" pattern="%.2f %unit%"/>
</channel-type>
<channel-type id="rainThisMonth" advanced="true">
<item-type>Number:Length</item-type>
<label>Rain This Month</label>
<description>Quantity of water this month</description>
<category>Rain</category>
<state readOnly="true" pattern="%.2f %unit%"/>
</channel-type>
<channel-type id="WindAngle">
<item-type>Number:Angle</item-type>
<label>Wind Angle</label>
<description>Current 5 minutes average wind direction</description>
<category>Wind</category>
<state min="0" max="360" step="1" readOnly="true" pattern="%d %unit%"/>
</channel-type>
<channel-type id="WindStrength">
<item-type>Number:Speed</item-type>
<label>Wind Strength</label>
<description>Current 5 minutes average wind speed</description>
<category>Wind</category>
<state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="MaxWindStrength" advanced="true">
<item-type>Number:Speed</item-type>
<label>Max Wind Strength</label>
<description>Maximum wind strength recorded</description>
<category>Wind</category>
<state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="DateMaxWindStrength" advanced="true">
<item-type>DateTime</item-type>
<label>Date Max Wind Strength</label>
<description>Timestamp when MaxWindStrength was recorded.</description>
<state readOnly="true" pattern="%1$td.%1$tm.%1$tY %1$tH:%1$tM"/>
</channel-type>
<channel-type id="GustAngle">
<item-type>Number:Angle</item-type>
<label>Gust Angle</label>
<description>Direction of the last 5 minutes highest gust wind</description>
<category>Wind</category>
<state min="0" max="360" step="1" readOnly="true" pattern="%d %unit%"/>
</channel-type>
<channel-type id="GustStrength">
<item-type>Number:Speed</item-type>
<label>Gust Strength</label>
<description>Speed of the last 5 minutes highest gust wind</description>
<category>Wind</category>
<state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="homeEvent">
<kind>trigger</kind>
<label>Home Event</label>
<description>Home event</description>
<event>
<options>
<option value="PERSON">person</option>
<option value="PERSON_AWAY">person_away</option>
<option value="MOVEMENT">movement</option>
<option value="CONNECTION">connection</option>
<option value="DISCONNECTION">disconnection</option>
<option value="ON">on</option>
<option value="OFF">off</option>
<option value="BOOT">boot</option>
<option value="SD">sd</option>
<option value="ALIM">alim</option>
<option value="NEW_MODULE">new_module</option>
<option value="MODULE_CONNECT">module_connect</option>
<option value="MODULE_DISCONNECT">module_disconnect</option>
<option value="MODULE_LOW_BATTERY">module_low_battery</option>
<option value="MODULE_END_UPDATE">module_end_update</option>
<option value="TAG_BIG_MOVE">tag_big_move</option>
<option value="TAG_SMALL_MOVE">tag_small_move</option>
<option value="TAG_UNINSTALLED">tag_uninstalled</option>
<option value="TAG_OPEN">tag_open</option>
</options>
</event>
</channel-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="netatmo"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="NHC">
<supported-bridge-type-refs>
<bridge-type-ref id="netatmoapi"/>
</supported-bridge-type-refs>
<label>Healthy Home Coach</label>
<description>This represents the healthy home coach capable of reporting health
index,temperature,humidity,pressure,air quality and sound level</description>
<channels>
<channel id="HealthIndex" typeId="healthindex"/>
<channel id="Co2" typeId="co2"/>
<channel id="Temperature" typeId="temperature"/>
<channel id="TempTrend" typeId="temperatureTrend"/>
<channel id="Noise" typeId="noise"/>
<channel id="Pressure" typeId="pressure"/>
<channel id="PressTrend" typeId="pressureTrend"/>
<channel id="AbsolutePressure" typeId="absolutePressure"/>
<channel id="TimeStamp" typeId="timeUtc"/>
<channel id="Humidity" typeId="humidity"/>
<channel id="MinTemp" typeId="minTemp"/>
<channel id="MaxTemp" typeId="maxTemp"/>
<channel id="DateMinTemp" typeId="dateMinTemp"/>
<channel id="DateMaxTemp" typeId="dateMaxTemp"/>
<!-- Common to all devices -->
<channel id="LastStatusStore" typeId="lastStatusStore"/>
<channel id="WifiStatus" typeId="system.signal-strength"/>
<channel id="Location" typeId="location"/>
</channels>
<properties>
<property name="signalLevels">86,71,56</property>
<property name="refreshPeriod">auto</property>
</properties>
<representation-property>id</representation-property>
<config-description-ref uri="thing-type:netatmo:station"/>
</thing-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,295 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="netatmo"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="NAMain">
<supported-bridge-type-refs>
<bridge-type-ref id="netatmoapi"/>
</supported-bridge-type-refs>
<label>Main Indoor Station</label>
<description>This represents the main indoor module capable of reporting temperature,humidity,pressure,air quality and
sound level</description>
<channels>
<channel id="Co2" typeId="co2"/>
<channel id="MinCo2" typeId="minCo2"/>
<channel id="MinCo2ThisWeek" typeId="minCo2ThisWeek"/>
<channel id="MinCo2ThisMonth" typeId="minCo2ThisMonth"/>
<channel id="MaxCo2" typeId="maxCo2"/>
<channel id="MaxCo2ThisWeek" typeId="maxCo2ThisWeek"/>
<channel id="MaxCo2ThisMonth" typeId="maxCo2ThisMonth"/>
<channel id="DateMinCo2" typeId="dateMinCo2"/>
<channel id="DateMinCo2ThisWeek" typeId="dateMinCo2ThisWeek"/>
<channel id="DateMinCo2ThisMonth" typeId="dateMinCo2ThisMonth"/>
<channel id="DateMaxCo2" typeId="dateMaxCo2"/>
<channel id="DateMaxCo2ThisWeek" typeId="dateMaxCo2ThisWeek"/>
<channel id="DateMaxCo2ThisMonth" typeId="dateMaxCo2ThisMonth"/>
<channel id="Temperature" typeId="temperature"/>
<channel id="TempTrend" typeId="temperatureTrend"/>
<channel id="Noise" typeId="noise"/>
<channel id="MinNoise" typeId="minNoise"/>
<channel id="MinNoiseThisWeek" typeId="minNoiseThisWeek"/>
<channel id="MinNoiseThisMonth" typeId="minNoiseThisMonth"/>
<channel id="MaxNoise" typeId="maxNoise"/>
<channel id="MaxNoiseThisWeek" typeId="maxNoiseThisWeek"/>
<channel id="MaxNoiseThisMonth" typeId="maxNoiseThisMonth"/>
<channel id="DateMinNoise" typeId="dateMinNoise"/>
<channel id="DateMinNoiseThisWeek" typeId="dateMinNoiseThisWeek"/>
<channel id="DateMinNoiseThisMonth" typeId="dateMinNoiseThisMonth"/>
<channel id="DateMaxNoise" typeId="dateMaxNoise"/>
<channel id="DateMaxNoiseThisWeek" typeId="dateMaxNoiseThisWeek"/>
<channel id="DateMaxNoiseThisMonth" typeId="dateMaxNoiseThisMonth"/>
<channel id="Pressure" typeId="pressure"/>
<channel id="MinPressure" typeId="minPressure"/>
<channel id="MinPressureThisWeek" typeId="minPressureThisWeek"/>
<channel id="MinPressureThisMonth" typeId="minPressureThisMonth"/>
<channel id="MaxPressure" typeId="maxPressure"/>
<channel id="MaxPressureThisWeek" typeId="maxPressureThisWeek"/>
<channel id="MaxPressureThisMonth" typeId="maxPressureThisMonth"/>
<channel id="DateMinPressure" typeId="dateMinPressure"/>
<channel id="DateMinPressureThisWeek" typeId="dateMinPressureThisWeek"/>
<channel id="DateMinPressureThisMonth" typeId="dateMinPressureThisMonth"/>
<channel id="DateMaxPressure" typeId="dateMaxPressure"/>
<channel id="DateMaxPressureThisWeek" typeId="dateMaxPressureThisWeek"/>
<channel id="DateMaxPressureThisMonth" typeId="dateMaxPressureThisMonth"/>
<channel id="PressTrend" typeId="pressureTrend"/>
<channel id="AbsolutePressure" typeId="absolutePressure"/>
<channel id="Humidity" typeId="humidity"/>
<channel id="MinHumidity" typeId="minHumidity"/>
<channel id="MinHumidityThisWeek" typeId="minHumidityThisWeek"/>
<channel id="MinHumidityThisMonth" typeId="minHumidityThisMonth"/>
<channel id="MaxHumidity" typeId="maxHumidity"/>
<channel id="MaxHumidityThisWeek" typeId="maxHumidityThisWeek"/>
<channel id="MaxHumidityThisMonth" typeId="maxHumidityThisMonth"/>
<channel id="DateMinHumidity" typeId="dateMinHumidity"/>
<channel id="DateMinHumidityThisWeek" typeId="dateMinHumidityThisWeek"/>
<channel id="DateMinHumidityThisMonth" typeId="dateMinHumidityThisMonth"/>
<channel id="DateMaxHumidity" typeId="dateMaxHumidity"/>
<channel id="DateMaxHumidityThisWeek" typeId="dateMaxHumidityThisWeek"/>
<channel id="DateMaxHumidityThisMonth" typeId="dateMaxHumidityThisMonth"/>
<channel id="Humidex" typeId="humidex"/>
<channel id="HeatIndex" typeId="heatIndex"/>
<channel id="Dewpoint" typeId="dewPoint"/>
<channel id="DewpointDepression" typeId="dewPointDepression"/>
<channel id="MinTemp" typeId="minTemp"/>
<channel id="MinTempThisWeek" typeId="minTempThisWeek"/>
<channel id="MinTempThisMonth" typeId="minTempThisMonth"/>
<channel id="MaxTemp" typeId="maxTemp"/>
<channel id="MaxTempThisWeek" typeId="maxTempThisWeek"/>
<channel id="MaxTempThisMonth" typeId="maxTempThisMonth"/>
<channel id="DateMinTemp" typeId="dateMinTemp"/>
<channel id="DateMinTempThisWeek" typeId="dateMinTempThisWeek"/>
<channel id="DateMinTempThisMonth" typeId="dateMinTempThisMonth"/>
<channel id="DateMaxTemp" typeId="dateMaxTemp"/>
<channel id="DateMaxTempThisWeek" typeId="dateMaxTempThisWeek"/>
<channel id="DateMaxTempThisMonth" typeId="dateMaxTempThisMonth"/>
<!-- Common to all devices -->
<channel id="TimeStamp" typeId="timeUtc"/>
<channel id="LastStatusStore" typeId="lastStatusStore"/>
<channel id="WifiStatus" typeId="system.signal-strength"/>
<channel id="Location" typeId="location"/>
</channels>
<properties>
<property name="signalLevels">86,71,56</property>
<property name="refreshPeriod">auto</property>
</properties>
<representation-property>id</representation-property>
<config-description-ref uri="thing-type:netatmo:station"/>
</thing-type>
<thing-type id="NAModule1">
<supported-bridge-type-refs>
<bridge-type-ref id="netatmoapi"/>
</supported-bridge-type-refs>
<label>Outdoor Module</label>
<description>This represents the outdoor module capable of reporting temperature and humidity</description>
<channels>
<channel id="Temperature" typeId="temperature"/>
<channel id="TempTrend" typeId="temperatureTrend"/>
<channel id="Humidity" typeId="humidity"/>
<channel id="MinHumidity" typeId="minHumidity"/>
<channel id="MinHumidityThisWeek" typeId="minHumidityThisWeek"/>
<channel id="MinHumidityThisMonth" typeId="minHumidityThisMonth"/>
<channel id="MaxHumidity" typeId="maxHumidity"/>
<channel id="MaxHumidityThisWeek" typeId="maxHumidityThisWeek"/>
<channel id="MaxHumidityThisMonth" typeId="maxHumidityThisMonth"/>
<channel id="DateMinHumidity" typeId="dateMinHumidity"/>
<channel id="DateMinHumidityThisWeek" typeId="dateMinHumidityThisWeek"/>
<channel id="DateMinHumidityThisMonth" typeId="dateMinHumidityThisMonth"/>
<channel id="DateMaxHumidity" typeId="dateMaxHumidity"/>
<channel id="DateMaxHumidityThisWeek" typeId="dateMaxHumidityThisWeek"/>
<channel id="DateMaxHumidityThisMonth" typeId="dateMaxHumidityThisMonth"/>
<channel id="Humidex" typeId="humidex"/>
<channel id="HeatIndex" typeId="heatIndex"/>
<channel id="Dewpoint" typeId="dewPoint"/>
<channel id="DewpointDepression" typeId="dewPointDepression"/>
<channel id="MinTemp" typeId="minTemp"/>
<channel id="MinTempThisWeek" typeId="minTempThisWeek"/>
<channel id="MinTempThisMonth" typeId="minTempThisMonth"/>
<channel id="MaxTemp" typeId="maxTemp"/>
<channel id="MaxTempThisWeek" typeId="maxTempThisWeek"/>
<channel id="MaxTempThisMonth" typeId="maxTempThisMonth"/>
<channel id="DateMinTemp" typeId="dateMinTemp"/>
<channel id="DateMinTempThisWeek" typeId="dateMinTempThisWeek"/>
<channel id="DateMinTempThisMonth" typeId="dateMinTempThisMonth"/>
<channel id="DateMaxTemp" typeId="dateMaxTemp"/>
<channel id="DateMaxTempThisWeek" typeId="dateMaxTempThisWeek"/>
<channel id="DateMaxTempThisMonth" typeId="dateMaxTempThisMonth"/>
<!-- Common to all modules -->
<channel id="TimeStamp" typeId="timeUtc"/>
<channel id="LastMessage" typeId="lastMessage"/>
<channel id="LowBattery" typeId="system.low-battery"/>
<channel id="BatteryVP" typeId="system.battery-level"/>
<channel id="RfStatus" typeId="system.signal-strength"/>
</channels>
<properties>
<property name="signalLevels">90,80,70,60</property>
<property name="batteryLevels">3600,4500,6000</property>
</properties>
<representation-property>id</representation-property>
<config-description-ref uri="thing-type:netatmo:module"/>
</thing-type>
<thing-type id="NAModule2">
<supported-bridge-type-refs>
<bridge-type-ref id="netatmoapi"/>
</supported-bridge-type-refs>
<label>Wind Gauge Module</label>
<description>This represents the wind module capable of reporting wind angle and strength</description>
<channels>
<channel id="WindAngle" typeId="WindAngle"/>
<channel id="WindStrength" typeId="WindStrength"/>
<channel id="MaxWindStrength" typeId="MaxWindStrength"/>
<channel id="DateMaxWindStrength" typeId="DateMaxWindStrength"/>
<channel id="GustAngle" typeId="GustAngle"/>
<channel id="GustStrength" typeId="GustStrength"/>
<!-- Common to all modules -->
<channel id="TimeStamp" typeId="timeUtc"/>
<channel id="LastMessage" typeId="lastMessage"/>
<channel id="LowBattery" typeId="system.low-battery"/>
<channel id="BatteryVP" typeId="system.battery-level"/>
<channel id="RfStatus" typeId="system.signal-strength"/>
</channels>
<properties>
<property name="signalLevels">90,80,70,60</property>
<property name="batteryLevels">3950,4770,6000</property>
</properties>
<representation-property>id</representation-property>
<config-description-ref uri="thing-type:netatmo:module"/>
</thing-type>
<thing-type id="NAModule3">
<supported-bridge-type-refs>
<bridge-type-ref id="netatmoapi"/>
</supported-bridge-type-refs>
<label>Rain Gauge</label>
<description>This represents the Rain Gauge capable of measuring precipitation</description>
<channels>
<channel id="Rain" typeId="rain"/>
<channel id="SumRain1" typeId="rain1"/>
<channel id="SumRain24" typeId="rain24"/>
<channel id="SumRainThisWeek" typeId="rainThisWeek"/>
<channel id="SumRainThisMonth" typeId="rainThisMonth"/>
<!-- Common to all modules -->
<channel id="TimeStamp" typeId="timeUtc"/>
<channel id="LastMessage" typeId="lastMessage"/>
<channel id="LowBattery" typeId="system.low-battery"/>
<channel id="BatteryVP" typeId="system.battery-level"/>
<channel id="RfStatus" typeId="system.signal-strength"/>
</channels>
<properties>
<property name="signalLevels">90,80,70,60</property>
<property name="batteryLevels">3600,4500,6000</property>
</properties>
<representation-property>id</representation-property>
<config-description-ref uri="thing-type:netatmo:module"/>
</thing-type>
<thing-type id="NAModule4">
<supported-bridge-type-refs>
<bridge-type-ref id="netatmoapi"/>
</supported-bridge-type-refs>
<label>Additional Module</label>
<description>This represents an additional indoor module capable of reporting temperature, humidity and CO2 level</description>
<channels>
<channel id="Co2" typeId="co2"/>
<channel id="MinCo2" typeId="minCo2"/>
<channel id="MinCo2ThisWeek" typeId="minCo2ThisWeek"/>
<channel id="MinCo2ThisMonth" typeId="minCo2ThisMonth"/>
<channel id="MaxCo2" typeId="maxCo2"/>
<channel id="MaxCo2ThisWeek" typeId="maxCo2ThisWeek"/>
<channel id="MaxCo2ThisMonth" typeId="maxCo2ThisMonth"/>
<channel id="DateMinCo2" typeId="dateMinCo2"/>
<channel id="DateMinCo2ThisWeek" typeId="dateMinCo2ThisWeek"/>
<channel id="DateMinCo2ThisMonth" typeId="dateMinCo2ThisMonth"/>
<channel id="DateMaxCo2" typeId="dateMaxCo2"/>
<channel id="DateMaxCo2ThisWeek" typeId="dateMaxCo2ThisWeek"/>
<channel id="DateMaxCo2ThisMonth" typeId="dateMaxCo2ThisMonth"/>
<channel id="Temperature" typeId="temperature"/>
<channel id="TempTrend" typeId="temperatureTrend"/>
<channel id="Humidity" typeId="humidity"/>
<channel id="MinHumidity" typeId="minHumidity"/>
<channel id="MinHumidityThisWeek" typeId="minHumidityThisWeek"/>
<channel id="MinHumidityThisMonth" typeId="minHumidityThisMonth"/>
<channel id="MaxHumidity" typeId="maxHumidity"/>
<channel id="MaxHumidityThisWeek" typeId="maxHumidityThisWeek"/>
<channel id="MaxHumidityThisMonth" typeId="maxHumidityThisMonth"/>
<channel id="DateMinHumidity" typeId="dateMinHumidity"/>
<channel id="DateMinHumidityThisWeek" typeId="dateMinHumidityThisWeek"/>
<channel id="DateMinHumidityThisMonth" typeId="dateMinHumidityThisMonth"/>
<channel id="DateMaxHumidity" typeId="dateMaxHumidity"/>
<channel id="DateMaxHumidityThisWeek" typeId="dateMaxHumidityThisWeek"/>
<channel id="DateMaxHumidityThisMonth" typeId="dateMaxHumidityThisMonth"/>
<channel id="Humidex" typeId="humidex"/>
<channel id="HeatIndex" typeId="heatIndex"/>
<channel id="Dewpoint" typeId="dewPoint"/>
<channel id="DewpointDepression" typeId="dewPointDepression"/>
<channel id="MinTemp" typeId="minTemp"/>
<channel id="MinTempThisWeek" typeId="minTempThisWeek"/>
<channel id="MinTempThisMonth" typeId="minTempThisMonth"/>
<channel id="MaxTemp" typeId="maxTemp"/>
<channel id="MaxTempThisWeek" typeId="maxTempThisWeek"/>
<channel id="MaxTempThisMonth" typeId="maxTempThisMonth"/>
<channel id="DateMinTemp" typeId="dateMinTemp"/>
<channel id="DateMinTempThisWeek" typeId="dateMinTempThisWeek"/>
<channel id="DateMinTempThisMonth" typeId="dateMinTempThisMonth"/>
<channel id="DateMaxTemp" typeId="dateMaxTemp"/>
<channel id="DateMaxTempThisWeek" typeId="dateMaxTempThisWeek"/>
<channel id="DateMaxTempThisMonth" typeId="dateMaxTempThisMonth"/>
<!-- Common to all modules -->
<channel id="TimeStamp" typeId="timeUtc"/>
<channel id="LastMessage" typeId="lastMessage"/>
<channel id="LowBattery" typeId="system.low-battery"/>
<channel id="BatteryVP" typeId="system.battery-level"/>
<channel id="RfStatus" typeId="system.signal-strength"/>
</channels>
<properties>
<property name="signalLevels">90,80,70,60</property>
<property name="batteryLevels">4200,4920,6000</property>
</properties>
<representation-property>id</representation-property>
<config-description-ref uri="thing-type:netatmo:module"/>
</thing-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,69 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="netatmo"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<!-- Netatmo Thermostat and Relay/Plug -->
<thing-type id="NAPlug">
<supported-bridge-type-refs>
<bridge-type-ref id="netatmoapi"/>
</supported-bridge-type-refs>
<label>Thermostat Relay/Plug</label>
<description>This represents the thermostat relay</description>
<channels>
<channel id="ConnectedBoiler" typeId="connectedBoiler"/>
<channel id="LastPlugSeen" typeId="lastPlugSeen"/>
<channel id="LastBilan" typeId="lastBilan"/>
<!-- Common to all devices -->
<channel id="LastStatusStore" typeId="lastStatusStore"/>
<channel id="WifiStatus" typeId="system.signal-strength"/>
<channel id="Location" typeId="location"/>
</channels>
<properties>
<property name="signalLevels">86,71,56</property>
<property name="refreshPeriod">3600000</property>
</properties>
<representation-property>id</representation-property>
<config-description-ref uri="thing-type:netatmo:plug"/>
</thing-type>
<thing-type id="NATherm1">
<supported-bridge-type-refs>
<bridge-type-ref id="netatmoapi"/>
</supported-bridge-type-refs>
<label>Thermostat Module</label>
<description>This represents the thermostat module itself</description>
<channels>
<channel id="Temperature" typeId="temperature"/>
<channel id="Sp_Temperature" typeId="setpointTemp"/>
<channel id="SetpointMode" typeId="setpointMode"/>
<channel id="ThermRelayCmd" typeId="ThermRelayCmd"/>
<channel id="ThermOrientation" typeId="ThermOrientation"/>
<channel id="TimeStamp" typeId="timeUtc"/>
<channel id="Planning" typeId="planning"/>
<channel id="SetpointEndTime" typeId="setpointEndTime"/>
<!-- Common to all modules -->
<channel id="LastMessage" typeId="lastMessage"/>
<channel id="LowBattery" typeId="system.low-battery"/>
<channel id="BatteryVP" typeId="system.battery-level"/>
<channel id="RfStatus" typeId="system.signal-strength"/>
</channels>
<properties>
<property name="signalLevels">90,80,70,60</property>
<property name="batteryLevels">2700,3300,4500</property>
</properties>
<representation-property>id</representation-property>
<config-description-ref uri="thing-type:netatmo:natherm1"/>
</thing-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,156 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="netatmo"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="NAWelcomeHome">
<supported-bridge-type-refs>
<bridge-type-ref id="netatmoapi"/>
</supported-bridge-type-refs>
<label>Welcome Home</label>
<description>This represents a home hosting a camera</description>
<channels>
<channel id="welcomeHomeCity" typeId="homecity"></channel>
<channel id="welcomeHomeCountry" typeId="homecountry"></channel>
<channel id="welcomeHomeTimezone" typeId="hometimezone"></channel>
<channel id="welcomeHomePersonCount" typeId="homepersoncount"></channel>
<channel id="welcomeHomeUnknownCount" typeId="homeunknowncount"></channel>
<channel id="welcomeEventType" typeId="type"></channel>
<channel id="welcomeEventTime" typeId="time"></channel>
<channel id="welcomeEventCameraId" typeId="camera_id"></channel>
<channel id="welcomeEventPersonId" typeId="person_id"></channel>
<channel id="welcomeEventSnapshot" typeId="snapshot"></channel>
<channel id="welcomeEventSnapshotURL" typeId="snapshot_url"></channel>
<channel id="welcomeEventVideoURL" typeId="video_url"></channel>
<channel id="welcomeEventVideoStatus" typeId="video_status"></channel>
<channel id="welcomeEventIsArrival" typeId="is_arrival"></channel>
<channel id="welcomeEventMessage" typeId="message"></channel>
<channel id="welcomeEventSubType" typeId="sub_type"></channel>
<channel id="welcomeHomeEvent" typeId="homeEvent"></channel>
<channel id="cameraEvent" typeId="cameraEvent"/>
</channels>
<representation-property>id</representation-property>
<config-description-ref uri="thing-type:netatmo:welcomehome"/>
</thing-type>
<channel-type id="homecity">
<item-type>String</item-type>
<label>City</label>
<description>City of the home</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="homecountry">
<item-type>String</item-type>
<label>Country</label>
<description>Country of the home</description>
<state readOnly="false"></state>
</channel-type>
<channel-type id="hometimezone">
<item-type>String</item-type>
<label>Timezone</label>
<description>Timezone of the home</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="homepersoncount" advanced="false">
<item-type>Number</item-type>
<label>Person Counter</label>
<description>Total number of Persons that are at home</description>
<state readOnly="true" pattern="%d"></state>
</channel-type>
<channel-type id="homeunknowncount" advanced="true">
<item-type>Number</item-type>
<label>Unknown Person Counter</label>
<description>Count how many Unknown Persons are at home</description>
<state readOnly="true" pattern="%d"></state>
</channel-type>
<channel-type id="type">
<item-type>String</item-type>
<label>Type</label>
<description>Type of event. Go to the Welcome page for further details.</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="time">
<item-type>DateTime</item-type>
<label>Time</label>
<description>Time of occurrence of event</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="camera_id">
<item-type>String</item-type>
<label>Camera ID</label>
<description>Camera that detected the event</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="person_id">
<item-type>String</item-type>
<label>Person ID</label>
<description>Id of the person the event is about (if any)</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="snapshot_url" advanced="true">
<item-type>String</item-type>
<label>Snapshot URL</label>
<description>Url of the event snapshot</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="snapshot">
<item-type>Image</item-type>
<label>Event Snapshot</label>
<description>Event Snapshot</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="video_url" advanced="true">
<item-type>String</item-type>
<label>Video URL</label>
<description>URL of the event video</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="video_status">
<item-type>String</item-type>
<label>Video Status</label>
<description>Status of the video (recording, deleted or available)</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="is_arrival">
<item-type>Switch</item-type>
<label>Is Arrival</label>
<description>If person was considered "away" before being seen during this event</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="message">
<item-type>String</item-type>
<label>Message</label>
<description>Message sent by Netatmo corresponding to given event</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="sub_type" advanced="true">
<item-type>String</item-type>
<label>Sub Type</label>
<description>Sub-type of SD and Alim events. Go to Welcome page for further details.</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="cameraEvent">
<kind>trigger</kind>
<label>Camera Event</label>
<event>
<options>
<option value="ANIMAL">Animal detected</option>
<option value="HUMAN">Human detected</option>
<option value="MOVEMENT">Unspecified movement detected</option>
<option value="VEHICLE">Vehicle detected</option>
</options>
</event>
</channel-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,86 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="netatmo"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="NAWelcomePerson">
<supported-bridge-type-refs>
<bridge-type-ref id="netatmoapi"/>
</supported-bridge-type-refs>
<label>Welcome Person</label>
<description>This represents a person at home</description>
<channels>
<channel id="welcomePersonLastSeen" typeId="last_seen"></channel>
<channel id="welcomePersonAtHome" typeId="person_athome"></channel>
<channel id="welcomePersonAvatarUrl" typeId="person_avatar_url"></channel>
<channel id="welcomePersonAvatar" typeId="person_avatar"></channel>
<channel id="welcomePersonLastEventMessage" typeId="person_eventmsg"></channel>
<channel id="welcomePersonLastEventTime" typeId="person_eventtime"></channel>
<channel id="welcomePersonLastEventUrl" typeId="person_event_url"></channel>
<channel id="welcomePersonLastEvent" typeId="person_event"></channel>
<channel id="welcomeHomeEvent" typeId="homeEvent"></channel>
</channels>
<representation-property>id</representation-property>
<config-description-ref uri="thing-type:netatmo:nawelcomeperson"/>
</thing-type>
<channel-type id="last_seen">
<item-type>DateTime</item-type>
<label>Last Seen</label>
<description>Time when this person was last seen</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="person_athome">
<item-type>Switch</item-type>
<label>At Home</label>
<description>Indicates if this person is known to be at home or not</description>
</channel-type>
<channel-type id="person_eventmsg">
<item-type>String</item-type>
<label>Last Event Message</label>
<description>Last Event message from this person</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="person_eventtime">
<item-type>DateTime</item-type>
<label>Last Event Time</label>
<description>Last Event message time for this person</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="person_avatar_url" advanced="true">
<item-type>String</item-type>
<label>Avatar URL</label>
<description>URL for the avatar of this person</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="person_avatar">
<item-type>Image</item-type>
<label>Avatar</label>
<description>Avatar of this person</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="person_event">
<item-type>Image</item-type>
<label>Last Event Picture</label>
<description>Picture of the last event for this person</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="person_event_url" advanced="true">
<item-type>String</item-type>
<label>Last Event URL</label>
<description>URL for the picture of the last event for this person</description>
<state readOnly="true"></state>
</channel-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,279 @@
/**
* Copyright (c) 2010-2020 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.junit.Assert.*;
import static org.mockito.Mockito.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.i18n.LocaleProvider;
import org.openhab.core.i18n.TranslationProvider;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ThingUID;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnitRunner;
import org.openhab.binding.netatmo.internal.handler.NetatmoBridgeHandler;
import io.swagger.client.model.NAMain;
import io.swagger.client.model.NAStationDataBody;
import io.swagger.client.model.NAStationModule;
/**
* @author Sven Strohschein - Initial contribution
*/
@RunWith(MockitoJUnitRunner.class)
public class NetatmoModuleDiscoveryServiceTest {
private NetatmoModuleDiscoveryServiceAccessible service;
private NetatmoBridgeHandler bridgeHandlerSpy;
@Before
public void before() {
Bridge bridgeMock = mock(Bridge.class);
when(bridgeMock.getUID()).thenReturn(new ThingUID("netatmo", "bridge"));
bridgeHandlerSpy = spy(new NetatmoBridgeHandler(bridgeMock, null));
LocaleProvider localeProviderMock = mock(LocaleProvider.class);
TranslationProvider translationProvider = mock(TranslationProvider.class);
service = new NetatmoModuleDiscoveryServiceAccessible(bridgeHandlerSpy, localeProviderMock,
translationProvider);
}
@Test
public void testStartScanNothingActivated() {
service.startScan();
assertEquals(0, service.getDiscoveredThings().size());
}
@Test
public void testStartScanDiscoverWeatherStationNoStationsBody() {
activateDiscoveryWeatherStation();
service.startScan();
assertEquals(0, service.getDiscoveredThings().size());
}
@Test
public void testStartScanDiscoverWeatherStationNoStations() {
activateDiscoveryWeatherStation();
when(bridgeHandlerSpy.getStationsDataBody(null)).thenReturn(Optional.of(new NAStationDataBody()));
service.startScan();
assertEquals(0, service.getDiscoveredThings().size());
}
@Test
public void testStartScanDiscoverWeatherStationNoStationName() {
recordStationBody(createStation());
service.startScan();
List<DiscoveryResult> discoveredThings = service.getDiscoveredThings();
assertEquals(1, discoveredThings.size());
// Expected is only the type name, because a station name isn't available
assertEquals("NAMain", discoveredThings.get(0).getLabel());
}
@Test
public void testStartScanDiscoverWeatherStation() {
NAMain station = createStation();
station.setStationName("Neu Wulmstorf");
recordStationBody(station);
service.startScan();
List<DiscoveryResult> discoveredThings = service.getDiscoveredThings();
assertEquals(1, discoveredThings.size());
// Expected is the type name + station name, because both are available
// and the station name contains only the city name by default which wouldn't be sufficient.
assertEquals("NAMain Neu Wulmstorf", discoveredThings.get(0).getLabel());
}
@Test
public void testStartScanDiscoverWeatherStationNoStationNameFavorite() {
NAMain station = createStation();
station.setFavorite(true);
recordStationBody(station);
service.startScan();
List<DiscoveryResult> discoveredThings = service.getDiscoveredThings();
assertEquals(1, discoveredThings.size());
// Expected is "(favorite)" within the label to make clear that it is favorite station
// (and not the station of the user)
assertEquals("NAMain (favorite)", discoveredThings.get(0).getLabel());
}
@Test
public void testStartScanDiscoverWeatherStationFavorite() {
NAMain station = createStation();
station.setStationName("Neu Wulmstorf");
station.setFavorite(true);
recordStationBody(station);
service.startScan();
List<DiscoveryResult> discoveredThings = service.getDiscoveredThings();
assertEquals(1, discoveredThings.size());
// Expected is "(favorite)" within the label to make clear that it is favorite station
// (and not the station of the user)
assertEquals("NAMain Neu Wulmstorf (favorite)", discoveredThings.get(0).getLabel());
}
@Test
public void testStartScanDiscoverWeatherStationModuleNoModuleName() {
NAMain station = createStation(createModule());
station.setStationName("Neu Wulmstorf");
recordStationBody(station);
service.startScan();
List<DiscoveryResult> discoveredThings = service.getDiscoveredThings();
assertEquals(2, discoveredThings.size());
assertEquals("NAMain Neu Wulmstorf", discoveredThings.get(0).getLabel());
// Expected is the type name + station name to make clear that the module belongs to the station.
// The module name isn't available, therefore the type name of the module is used.
assertEquals("NAModule1 Neu Wulmstorf", discoveredThings.get(1).getLabel());
}
@Test
public void testStartScanDiscoverWeatherStationModule() {
NAStationModule module = createModule();
module.setModuleName("Outdoor-Module");
NAMain station = createStation(module);
station.setStationName("Neu Wulmstorf");
recordStationBody(station);
service.startScan();
List<DiscoveryResult> discoveredThings = service.getDiscoveredThings();
assertEquals(2, discoveredThings.size());
assertEquals("NAMain Neu Wulmstorf", discoveredThings.get(0).getLabel());
// Expected is the module name + station name to make clear that the module belongs to the station.
// Because an explicit module name is available, the module type name isn't required.
assertEquals("Outdoor-Module Neu Wulmstorf", discoveredThings.get(1).getLabel());
}
@Test
public void testStartScanDiscoverWeatherStationModuleNoModuleNameFavorite() {
NAMain station = createStation(createModule());
station.setStationName("Neu Wulmstorf");
station.setFavorite(true);
recordStationBody(station);
service.startScan();
List<DiscoveryResult> discoveredThings = service.getDiscoveredThings();
assertEquals(2, discoveredThings.size());
assertEquals("NAMain Neu Wulmstorf (favorite)", discoveredThings.get(0).getLabel());
// Expected is "(favorite)" within the label to make clear that it is favorite station
// (and not the station of the user)
assertEquals("NAModule1 Neu Wulmstorf (favorite)", discoveredThings.get(1).getLabel());
}
@Test
public void testStartScanDiscoverWeatherStationModuleFavorite() {
NAStationModule module = createModule();
module.setModuleName("Outdoor-Module");
NAMain station = createStation(module);
station.setStationName("Neu Wulmstorf");
station.setFavorite(true);
recordStationBody(station);
service.startScan();
List<DiscoveryResult> discoveredThings = service.getDiscoveredThings();
assertEquals(2, discoveredThings.size());
assertEquals("NAMain Neu Wulmstorf (favorite)", discoveredThings.get(0).getLabel());
// Expected is "(favorite)" within the label to make clear that it is favorite station
// (and not the station of the user)
assertEquals("Outdoor-Module Neu Wulmstorf (favorite)", discoveredThings.get(1).getLabel());
}
private void recordStationBody(NAMain station) {
activateDiscoveryWeatherStation();
NAStationDataBody stationsBody = new NAStationDataBody();
stationsBody.setDevices(Collections.singletonList(station));
when(bridgeHandlerSpy.getStationsDataBody(null)).thenReturn(Optional.of(stationsBody));
}
private void activateDiscoveryWeatherStation() {
bridgeHandlerSpy.configuration.readStation = true;
}
private static NAMain createStation() {
NAMain station = new NAMain();
station.setId("01:00:00:00:00:aa");
station.setType("NAMain");
return station;
}
private static NAMain createStation(NAStationModule module) {
NAMain station = createStation();
station.setModules(Collections.singletonList(module));
return station;
}
private static NAStationModule createModule() {
NAStationModule module = new NAStationModule();
module.setId("01:00:00:00:01:aa");
module.setType("NAModule1");
return module;
}
@NonNullByDefault
private static class NetatmoModuleDiscoveryServiceAccessible extends NetatmoModuleDiscoveryService {
private final List<DiscoveryResult> discoveredThings;
private NetatmoModuleDiscoveryServiceAccessible(NetatmoBridgeHandler netatmoBridgeHandler,
LocaleProvider localeProvider, TranslationProvider translationProvider) {
super(netatmoBridgeHandler, localeProvider, translationProvider);
discoveredThings = new ArrayList<>();
}
@Override
protected void thingDiscovered(DiscoveryResult discoveryResult) {
super.thingDiscovered(discoveryResult);
discoveredThings.add(discoveryResult);
}
private List<DiscoveryResult> getDiscoveredThings() {
return discoveredThings;
}
}
}

View File

@@ -0,0 +1,493 @@
/**
* Copyright (c) 2010-2020 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.presence;
import static org.junit.Assert.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNull;
import org.openhab.core.i18n.TimeZoneProvider;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.internal.ThingImpl;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.openhab.binding.netatmo.internal.NetatmoBindingConstants;
import io.swagger.client.model.NAWelcomeCamera;
/**
* @author Sven Strohschein - Initial contribution
*/
@RunWith(MockitoJUnitRunner.class)
public class NAPresenceCameraHandlerTest {
private static final String DUMMY_VPN_URL = "https://dummytestvpnaddress.net/restricted/10.255.89.96/9826069dc689e8327ac3ed2ced4ff089/MTU5MTgzMzYwMDrQ7eHHhG0_OJ4TgmPhGlnK7QQ5pZ,,";
private static final String DUMMY_LOCAL_URL = "http://192.168.178.76/9826069dc689e8327ac3ed2ced4ff089";
private static final Optional<String> DUMMY_PING_RESPONSE = createPingResponseContent(DUMMY_LOCAL_URL);
@Mock
private RequestExecutor requestExecutorMock;
@Mock
private TimeZoneProvider timeZoneProviderMock;
private Thing presenceCameraThing;
private NAWelcomeCamera presenceCamera;
private ChannelUID cameraStatusChannelUID;
private ChannelUID floodlightChannelUID;
private ChannelUID floodlightAutoModeChannelUID;
private NAPresenceCameraHandlerAccessible handler;
@Before
public void before() {
presenceCameraThing = new ThingImpl(new ThingTypeUID("netatmo", "NOC"), "1");
presenceCamera = new NAWelcomeCamera();
cameraStatusChannelUID = new ChannelUID(presenceCameraThing.getUID(),
NetatmoBindingConstants.CHANNEL_CAMERA_STATUS);
floodlightChannelUID = new ChannelUID(presenceCameraThing.getUID(),
NetatmoBindingConstants.CHANNEL_CAMERA_FLOODLIGHT);
floodlightAutoModeChannelUID = new ChannelUID(presenceCameraThing.getUID(),
NetatmoBindingConstants.CHANNEL_CAMERA_FLOODLIGHT_AUTO_MODE);
handler = new NAPresenceCameraHandlerAccessible(presenceCameraThing, presenceCamera);
}
@Test
public void testHandleCommandSwitchSurveillanceOn() {
when(requestExecutorMock.executeGETRequest(DUMMY_VPN_URL + "/command/ping")).thenReturn(DUMMY_PING_RESPONSE);
presenceCamera.setVpnUrl(DUMMY_VPN_URL);
handler.handleCommand(cameraStatusChannelUID, OnOffType.ON);
verify(requestExecutorMock, times(2)).executeGETRequest(any()); // 1.) execute ping + 2.) execute switch on
verify(requestExecutorMock).executeGETRequest(DUMMY_LOCAL_URL + "/command/changestatus?status=on");
}
@Test
public void testHandleCommandSwitchSurveillanceOff() {
when(requestExecutorMock.executeGETRequest(DUMMY_VPN_URL + "/command/ping")).thenReturn(DUMMY_PING_RESPONSE);
presenceCamera.setVpnUrl(DUMMY_VPN_URL);
handler.handleCommand(cameraStatusChannelUID, OnOffType.OFF);
verify(requestExecutorMock, times(2)).executeGETRequest(any()); // 1.) execute ping + 2.) execute switch off
verify(requestExecutorMock).executeGETRequest(DUMMY_LOCAL_URL + "/command/changestatus?status=off");
}
@Test
public void testHandleCommandSwitchSurveillanceUnknownCommand() {
presenceCamera.setVpnUrl(DUMMY_VPN_URL);
handler.handleCommand(cameraStatusChannelUID, RefreshType.REFRESH);
verify(requestExecutorMock, never()).executeGETRequest(any()); // nothing should get executed on a refresh
// command
}
@Test
public void testHandleCommandSwitchSurveillanceWithoutVPN() {
handler.handleCommand(cameraStatusChannelUID, OnOffType.ON);
verify(requestExecutorMock, never()).executeGETRequest(any()); // nothing should get executed when no VPN
// address is set
}
@Test
public void testHandleCommandSwitchFloodlightOn() {
when(requestExecutorMock.executeGETRequest(DUMMY_VPN_URL + "/command/ping")).thenReturn(DUMMY_PING_RESPONSE);
presenceCamera.setVpnUrl(DUMMY_VPN_URL);
handler.handleCommand(floodlightChannelUID, OnOffType.ON);
verify(requestExecutorMock, times(2)).executeGETRequest(any()); // 1.) execute ping + 2.) execute switch on
verify(requestExecutorMock)
.executeGETRequest(DUMMY_LOCAL_URL + "/command/floodlight_set_config?config=%7B%22mode%22:%22on%22%7D");
}
@Test
public void testHandleCommandSwitchFloodlightOff() {
when(requestExecutorMock.executeGETRequest(DUMMY_VPN_URL + "/command/ping")).thenReturn(DUMMY_PING_RESPONSE);
presenceCamera.setVpnUrl(DUMMY_VPN_URL);
handler.handleCommand(floodlightChannelUID, OnOffType.OFF);
verify(requestExecutorMock, times(2)).executeGETRequest(any()); // 1.) execute ping + 2.) execute switch off
verify(requestExecutorMock).executeGETRequest(
DUMMY_LOCAL_URL + "/command/floodlight_set_config?config=%7B%22mode%22:%22off%22%7D");
}
@Test
public void testHandleCommandSwitchFloodlightOffWithAutoModeOn() {
when(requestExecutorMock.executeGETRequest(DUMMY_VPN_URL + "/command/ping")).thenReturn(DUMMY_PING_RESPONSE);
presenceCamera.setVpnUrl(DUMMY_VPN_URL);
presenceCamera.setLightModeStatus(NAWelcomeCamera.LightModeStatusEnum.AUTO);
assertEquals(OnOffType.ON, handler.getNAThingProperty(floodlightAutoModeChannelUID.getId()));
handler.handleCommand(floodlightChannelUID, OnOffType.OFF);
verify(requestExecutorMock, times(2)).executeGETRequest(any()); // 1.) execute ping + 2.) execute switch off
verify(requestExecutorMock).executeGETRequest(
DUMMY_LOCAL_URL + "/command/floodlight_set_config?config=%7B%22mode%22:%22auto%22%7D");
}
@Test
public void testHandleCommandSwitchFloodlightOnAddressChanged() {
when(requestExecutorMock.executeGETRequest(DUMMY_VPN_URL + "/command/ping")).thenReturn(DUMMY_PING_RESPONSE);
presenceCamera.setVpnUrl(DUMMY_VPN_URL);
handler.handleCommand(floodlightChannelUID, OnOffType.ON);
// 1.) execute ping + 2.) execute switch on
verify(requestExecutorMock, times(2)).executeGETRequest(any());
verify(requestExecutorMock)
.executeGETRequest(DUMMY_LOCAL_URL + "/command/floodlight_set_config?config=%7B%22mode%22:%22on%22%7D");
handler.handleCommand(floodlightChannelUID, OnOffType.OFF);
// 1.) execute ping + 2.) execute switch on + 3.) execute switch off
verify(requestExecutorMock, times(3)).executeGETRequest(any());
verify(requestExecutorMock)
.executeGETRequest(DUMMY_LOCAL_URL + "/command/floodlight_set_config?config=%7B%22mode%22:%22on%22%7D");
verify(requestExecutorMock).executeGETRequest(
DUMMY_LOCAL_URL + "/command/floodlight_set_config?config=%7B%22mode%22:%22off%22%7D");
final String newDummyVPNURL = DUMMY_VPN_URL + "2";
final String newDummyLocalURL = DUMMY_LOCAL_URL + "2";
final Optional<String> newDummyPingResponse = createPingResponseContent(newDummyLocalURL);
when(requestExecutorMock.executeGETRequest(newDummyVPNURL + "/command/ping")).thenReturn(newDummyPingResponse);
presenceCamera.setVpnUrl(newDummyVPNURL);
handler.handleCommand(floodlightChannelUID, OnOffType.ON);
// 1.) execute ping + 2.) execute switch on + 3.) execute switch off + 4.) execute ping + 5.) execute switch on
verify(requestExecutorMock, times(5)).executeGETRequest(any());
verify(requestExecutorMock)
.executeGETRequest(DUMMY_LOCAL_URL + "/command/floodlight_set_config?config=%7B%22mode%22:%22on%22%7D");
verify(requestExecutorMock).executeGETRequest(
DUMMY_LOCAL_URL + "/command/floodlight_set_config?config=%7B%22mode%22:%22off%22%7D");
verify(requestExecutorMock).executeGETRequest(
newDummyLocalURL + "/command/floodlight_set_config?config=%7B%22mode%22:%22on%22%7D");
}
@Test
public void testHandleCommandSwitchFloodlightUnknownCommand() {
presenceCamera.setVpnUrl(DUMMY_VPN_URL);
handler.handleCommand(floodlightChannelUID, RefreshType.REFRESH);
verify(requestExecutorMock, never()).executeGETRequest(any()); // nothing should get executed on a refresh
// command
}
@Test
public void testHandleCommandSwitchFloodlightAutoModeOn() {
when(requestExecutorMock.executeGETRequest(DUMMY_VPN_URL + "/command/ping")).thenReturn(DUMMY_PING_RESPONSE);
presenceCamera.setVpnUrl(DUMMY_VPN_URL);
handler.handleCommand(floodlightAutoModeChannelUID, OnOffType.ON);
verify(requestExecutorMock, times(2)).executeGETRequest(any()); // 1.) execute ping + 2.) execute switch
// auto-mode on
verify(requestExecutorMock).executeGETRequest(
DUMMY_LOCAL_URL + "/command/floodlight_set_config?config=%7B%22mode%22:%22auto%22%7D");
}
@Test
public void testHandleCommandSwitchFloodlightAutoModeOff() {
when(requestExecutorMock.executeGETRequest(DUMMY_VPN_URL + "/command/ping")).thenReturn(DUMMY_PING_RESPONSE);
presenceCamera.setVpnUrl(DUMMY_VPN_URL);
handler.handleCommand(floodlightAutoModeChannelUID, OnOffType.OFF);
verify(requestExecutorMock, times(2)).executeGETRequest(any()); // 1.) execute ping + 2.) execute switch off
verify(requestExecutorMock).executeGETRequest(
DUMMY_LOCAL_URL + "/command/floodlight_set_config?config=%7B%22mode%22:%22off%22%7D");
}
@Test
public void testHandleCommandSwitchFloodlightAutoModeUnknownCommand() {
presenceCamera.setVpnUrl(DUMMY_VPN_URL);
handler.handleCommand(floodlightAutoModeChannelUID, RefreshType.REFRESH);
verify(requestExecutorMock, never()).executeGETRequest(any()); // nothing should get executed on a refresh
// command
}
/**
* The request "fails" because there is no response content of the ping command.
*/
@Test
public void testHandleCommandRequestFailed() {
presenceCamera.setVpnUrl(DUMMY_VPN_URL);
handler.handleCommand(floodlightChannelUID, OnOffType.ON);
verify(requestExecutorMock, times(1)).executeGETRequest(any()); // 1.) execute ping
}
@Test
public void testHandleCommandWithoutVPN() {
handler.handleCommand(floodlightChannelUID, OnOffType.ON);
verify(requestExecutorMock, never()).executeGETRequest(any()); // no executions because the VPN URL is still
// unknown
}
@Test
public void testHandleCommandPingFailedNULLResponse() {
when(requestExecutorMock.executeGETRequest(DUMMY_VPN_URL + "/command/ping")).thenReturn(Optional.of(""));
presenceCamera.setVpnUrl(DUMMY_VPN_URL);
handler.handleCommand(floodlightChannelUID, OnOffType.ON);
verify(requestExecutorMock, times(1)).executeGETRequest(any()); // 1.) execute ping
}
@Test
public void testHandleCommandPingFailedEmptyResponse() {
when(requestExecutorMock.executeGETRequest(DUMMY_VPN_URL + "/command/ping")).thenReturn(Optional.of(""));
presenceCamera.setVpnUrl(DUMMY_VPN_URL);
handler.handleCommand(floodlightChannelUID, OnOffType.ON);
verify(requestExecutorMock, times(1)).executeGETRequest(any()); // 1.) execute ping
}
@Test
public void testHandleCommandPingFailedWrongResponse() {
when(requestExecutorMock.executeGETRequest(DUMMY_VPN_URL + "/command/ping"))
.thenReturn(Optional.of("{ \"message\": \"error\" }"));
presenceCamera.setVpnUrl(DUMMY_VPN_URL);
handler.handleCommand(floodlightChannelUID, OnOffType.ON);
verify(requestExecutorMock, times(1)).executeGETRequest(any()); // 1.) execute ping
}
@Test
public void testHandleCommandModuleNULL() {
NAPresenceCameraHandler handlerWithoutModule = new NAPresenceCameraHandler(presenceCameraThing,
timeZoneProviderMock);
handlerWithoutModule.handleCommand(floodlightChannelUID, OnOffType.ON);
verify(requestExecutorMock, never()).executeGETRequest(any()); // no executions because the thing isn't
// initialized
}
@Test
public void testGetNAThingPropertyCommonChannel() {
assertEquals(OnOffType.OFF, handler.getNAThingProperty(NetatmoBindingConstants.CHANNEL_CAMERA_STATUS));
}
@Test
public void testGetNAThingPropertyFloodlightOn() {
presenceCamera.setLightModeStatus(NAWelcomeCamera.LightModeStatusEnum.ON);
assertEquals(OnOffType.ON, handler.getNAThingProperty(floodlightChannelUID.getId()));
}
@Test
public void testGetNAThingPropertyFloodlightOff() {
presenceCamera.setLightModeStatus(NAWelcomeCamera.LightModeStatusEnum.OFF);
assertEquals(OnOffType.OFF, handler.getNAThingProperty(floodlightChannelUID.getId()));
}
@Test
public void testGetNAThingPropertyFloodlightAuto() {
presenceCamera.setLightModeStatus(NAWelcomeCamera.LightModeStatusEnum.AUTO);
// When the floodlight is set to auto-mode it is currently off.
assertEquals(OnOffType.OFF, handler.getNAThingProperty(floodlightChannelUID.getId()));
}
@Test
public void testGetNAThingPropertyFloodlightWithoutLightModeState() {
assertEquals(OnOffType.OFF, handler.getNAThingProperty(floodlightChannelUID.getId()));
}
@Test
public void testGetNAThingPropertyFloodlightModuleNULL() {
NAPresenceCameraHandler handlerWithoutModule = new NAPresenceCameraHandler(presenceCameraThing,
timeZoneProviderMock);
assertEquals(UnDefType.UNDEF, handlerWithoutModule.getNAThingProperty(floodlightChannelUID.getId()));
}
@Test
public void testGetNAThingPropertyFloodlightAutoModeFloodlightAuto() {
presenceCamera.setLightModeStatus(NAWelcomeCamera.LightModeStatusEnum.AUTO);
assertEquals(OnOffType.ON, handler.getNAThingProperty(floodlightAutoModeChannelUID.getId()));
}
@Test
public void testGetNAThingPropertyFloodlightAutoModeFloodlightOn() {
presenceCamera.setLightModeStatus(NAWelcomeCamera.LightModeStatusEnum.ON);
// When the floodlight is initially on (on starting the binding), there is no information about if the auto-mode
// was set before. Therefore the auto-mode is detected as deactivated / off.
assertEquals(OnOffType.OFF, handler.getNAThingProperty(floodlightAutoModeChannelUID.getId()));
}
@Test
public void testGetNAThingPropertyFloodlightAutoModeFloodlightOff() {
presenceCamera.setLightModeStatus(NAWelcomeCamera.LightModeStatusEnum.ON);
// When the floodlight is initially off (on starting the binding), the auto-mode isn't set.
assertEquals(OnOffType.OFF, handler.getNAThingProperty(floodlightAutoModeChannelUID.getId()));
}
@Test
public void testGetNAThingPropertyFloodlightScenarioWithAutoMode() {
presenceCamera.setLightModeStatus(NAWelcomeCamera.LightModeStatusEnum.AUTO);
assertEquals(OnOffType.ON, handler.getNAThingProperty(floodlightAutoModeChannelUID.getId()));
assertEquals(OnOffType.OFF, handler.getNAThingProperty(floodlightChannelUID.getId()));
// The auto-mode was initially set, after that the floodlight was switched on by the user.
// In this case the binding should still know that the auto-mode is/was set.
presenceCamera.setLightModeStatus(NAWelcomeCamera.LightModeStatusEnum.ON);
assertEquals(OnOffType.ON, handler.getNAThingProperty(floodlightAutoModeChannelUID.getId()));
assertEquals(OnOffType.ON, handler.getNAThingProperty(floodlightChannelUID.getId()));
// After that the user switched off the floodlight.
// In this case the binding should still know that the auto-mode is/was set.
presenceCamera.setLightModeStatus(NAWelcomeCamera.LightModeStatusEnum.OFF);
assertEquals(OnOffType.ON, handler.getNAThingProperty(floodlightAutoModeChannelUID.getId()));
assertEquals(OnOffType.OFF, handler.getNAThingProperty(floodlightChannelUID.getId()));
}
@Test
public void testGetNAThingPropertyFloodlightScenarioWithoutAutoMode() {
presenceCamera.setLightModeStatus(NAWelcomeCamera.LightModeStatusEnum.OFF);
assertEquals(OnOffType.OFF, handler.getNAThingProperty(floodlightAutoModeChannelUID.getId()));
assertEquals(OnOffType.OFF, handler.getNAThingProperty(floodlightChannelUID.getId()));
// The auto-mode wasn't set, after that the floodlight was switched on by the user.
// In this case the binding should still know that the auto-mode isn't/wasn't set.
presenceCamera.setLightModeStatus(NAWelcomeCamera.LightModeStatusEnum.ON);
assertEquals(OnOffType.OFF, handler.getNAThingProperty(floodlightAutoModeChannelUID.getId()));
assertEquals(OnOffType.ON, handler.getNAThingProperty(floodlightChannelUID.getId()));
// After that the user switched off the floodlight.
// In this case the binding should still know that the auto-mode isn't/wasn't set.
presenceCamera.setLightModeStatus(NAWelcomeCamera.LightModeStatusEnum.OFF);
assertEquals(OnOffType.OFF, handler.getNAThingProperty(floodlightAutoModeChannelUID.getId()));
assertEquals(OnOffType.OFF, handler.getNAThingProperty(floodlightChannelUID.getId()));
}
@Test
public void testGetNAThingPropertyFloodlightAutoModeModuleNULL() {
NAPresenceCameraHandler handlerWithoutModule = new NAPresenceCameraHandler(presenceCameraThing,
timeZoneProviderMock);
assertEquals(UnDefType.UNDEF, handlerWithoutModule.getNAThingProperty(floodlightAutoModeChannelUID.getId()));
}
@Test
public void testGetStreamURL() {
presenceCamera.setVpnUrl(DUMMY_VPN_URL);
Optional<String> streamURL = handler.getStreamURL("dummyVideoId");
assertTrue(streamURL.isPresent());
assertEquals(DUMMY_VPN_URL + "/vod/dummyVideoId/index.m3u8", streamURL.get());
}
@Test
public void testGetStreamURLLocal() {
presenceCamera.setVpnUrl(DUMMY_VPN_URL);
presenceCamera.setIsLocal(true);
Optional<String> streamURL = handler.getStreamURL("dummyVideoId");
assertTrue(streamURL.isPresent());
assertEquals(DUMMY_VPN_URL + "/vod/dummyVideoId/index_local.m3u8", streamURL.get());
}
@Test
public void testGetStreamURLNotLocal() {
presenceCamera.setVpnUrl(DUMMY_VPN_URL);
presenceCamera.setIsLocal(false);
Optional<String> streamURL = handler.getStreamURL("dummyVideoId");
assertTrue(streamURL.isPresent());
assertEquals(DUMMY_VPN_URL + "/vod/dummyVideoId/index.m3u8", streamURL.get());
}
@Test
public void testGetStreamURLWithoutVPN() {
Optional<String> streamURL = handler.getStreamURL("dummyVideoId");
assertFalse(streamURL.isPresent());
}
@Test
public void testGetLivePictureURLState() {
presenceCamera.setVpnUrl(DUMMY_VPN_URL);
State livePictureURLState = handler.getLivePictureURLState();
assertEquals(new StringType(DUMMY_VPN_URL + "/live/snapshot_720.jpg"), livePictureURLState);
}
@Test
public void testGetLivePictureURLStateWithoutVPN() {
State livePictureURLState = handler.getLivePictureURLState();
assertEquals(UnDefType.UNDEF, livePictureURLState);
}
@Test
public void testGetLiveStreamState() {
presenceCamera.setVpnUrl(DUMMY_VPN_URL);
State liveStreamState = handler.getLiveStreamState();
assertEquals(new StringType(DUMMY_VPN_URL + "/live/index.m3u8"), liveStreamState);
}
@Test
public void testGetLiveStreamStateWithoutVPN() {
State liveStreamState = handler.getLiveStreamState();
assertEquals(UnDefType.UNDEF, liveStreamState);
}
private static Optional<String> createPingResponseContent(final String localURL) {
return Optional.of("{\"local_url\":\"" + localURL + "\",\"product_name\":\"Welcome Netatmo\"}");
}
private interface RequestExecutor {
Optional<String> executeGETRequest(String url);
}
private class NAPresenceCameraHandlerAccessible extends NAPresenceCameraHandler {
private NAPresenceCameraHandlerAccessible(Thing thing, NAWelcomeCamera presenceCamera) {
super(thing, timeZoneProviderMock);
setModule(presenceCamera);
}
@Override
protected @NonNull Optional<@NonNull String> executeGETRequest(@NonNull String url) {
return requestExecutorMock.executeGETRequest(url);
}
@Override
protected @NonNull State getLivePictureURLState() {
return super.getLivePictureURLState();
}
@Override
protected @NonNull State getLiveStreamState() {
return super.getLiveStreamState();
}
}
}

View File

@@ -0,0 +1,368 @@
/**
* Copyright (c) 2010-2020 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.welcome;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import java.util.*;
import org.eclipse.jdt.annotation.NonNull;
import org.openhab.core.i18n.TimeZoneProvider;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.internal.ThingImpl;
import org.openhab.core.types.UnDefType;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.openhab.binding.netatmo.internal.NetatmoBindingConstants;
import org.openhab.binding.netatmo.internal.handler.NetatmoBridgeHandler;
import org.openhab.binding.netatmo.internal.webhook.NAWebhookCameraEvent;
import io.swagger.client.model.NAWelcomeEvent;
import io.swagger.client.model.NAWelcomeHome;
import io.swagger.client.model.NAWelcomeHomeData;
import io.swagger.client.model.NAWelcomeSubEvent;
/**
* @author Sven Strohschein - Initial contribution
*/
@RunWith(MockitoJUnitRunner.class)
public class NAWelcomeHomeHandlerTest {
private static final String DUMMY_HOME_ID = "1";
@Mock
private TimeZoneProvider timeZoneProviderMock;
private NAWelcomeHomeHandlerAccessible handler;
@Mock
private NetatmoBridgeHandler bridgeHandlerMock;
@Before
public void before() {
Thing welcomeHomeThing = new ThingImpl(new ThingTypeUID("netatmo", "NAWelcomeHome"), "1");
handler = new NAWelcomeHomeHandlerAccessible(welcomeHomeThing);
}
@Test
public void testUpdateReadingsWithEvents() {
NAWelcomeEvent event1 = createEvent(1592661881, NAWebhookCameraEvent.EventTypeEnum.PERSON);
NAWelcomeEvent event2 = createEvent(1592661882, NAWebhookCameraEvent.EventTypeEnum.MOVEMENT);
NAWelcomeHome home = new NAWelcomeHome();
home.setId(DUMMY_HOME_ID);
home.setEvents(Arrays.asList(event1, event2));
NAWelcomeHomeData homeData = new NAWelcomeHomeData();
homeData.setHomes(Collections.singletonList(home));
when(bridgeHandlerMock.getWelcomeDataBody(DUMMY_HOME_ID)).thenReturn(Optional.of(homeData));
handler.updateReadings();
// the second (last) event is expected
assertEquals(new StringType("movement"),
handler.getNAThingProperty(NetatmoBindingConstants.CHANNEL_WELCOME_EVENT_TYPE));
home.setEvents(Arrays.asList(event2, event1));
// the second (last) event is still expected (independent from the order of these are added)
assertEquals(new StringType("movement"),
handler.getNAThingProperty(NetatmoBindingConstants.CHANNEL_WELCOME_EVENT_TYPE));
}
@Test
public void testUpdateReadingsWith1Event() {
NAWelcomeEvent event = createEvent(1592661881, NAWebhookCameraEvent.EventTypeEnum.PERSON);
NAWelcomeHome home = new NAWelcomeHome();
home.setId(DUMMY_HOME_ID);
home.setEvents(Collections.singletonList(event));
NAWelcomeHomeData homeData = new NAWelcomeHomeData();
homeData.setHomes(Collections.singletonList(home));
when(bridgeHandlerMock.getWelcomeDataBody(DUMMY_HOME_ID)).thenReturn(Optional.of(homeData));
handler.updateReadings();
assertEquals(new StringType("person"),
handler.getNAThingProperty(NetatmoBindingConstants.CHANNEL_WELCOME_EVENT_TYPE));
}
@Test
public void testUpdateReadingsNoEvents() {
NAWelcomeHome home = new NAWelcomeHome();
home.setId(DUMMY_HOME_ID);
NAWelcomeHomeData homeData = new NAWelcomeHomeData();
homeData.setHomes(Collections.singletonList(home));
when(bridgeHandlerMock.getWelcomeDataBody(DUMMY_HOME_ID)).thenReturn(Optional.of(homeData));
handler.updateReadings();
assertEquals(UnDefType.UNDEF, handler.getNAThingProperty(NetatmoBindingConstants.CHANNEL_WELCOME_EVENT_TYPE));
}
@Test
public void testUpdateReadingsEmptyHomeData() {
NAWelcomeHomeData homeData = new NAWelcomeHomeData();
when(bridgeHandlerMock.getWelcomeDataBody(any())).thenReturn(Optional.of(homeData));
handler.updateReadings();
assertEquals(UnDefType.UNDEF, handler.getNAThingProperty(NetatmoBindingConstants.CHANNEL_WELCOME_EVENT_TYPE));
}
@Test
public void testUpdateReadingsNoHomeData() {
handler.updateReadings();
assertEquals(UnDefType.UNDEF, handler.getNAThingProperty(NetatmoBindingConstants.CHANNEL_WELCOME_EVENT_TYPE));
}
@Test
public void testTriggerChannelIfRequired() {
NAWelcomeEvent event1 = createPresenceEvent(1592661881, NAWelcomeSubEvent.TypeEnum.ANIMAL);
NAWelcomeEvent event2 = createPresenceEvent(1592661882, NAWelcomeSubEvent.TypeEnum.HUMAN);
NAWelcomeEvent event3 = createEvent(1592661883, NAWebhookCameraEvent.EventTypeEnum.MOVEMENT);
NAWelcomeHome home = new NAWelcomeHome();
home.setId(DUMMY_HOME_ID);
home.setEvents(Collections.singletonList(event1));
NAWelcomeHomeData homeData = new NAWelcomeHomeData();
homeData.setHomes(Collections.singletonList(home));
when(bridgeHandlerMock.getWelcomeDataBody(DUMMY_HOME_ID)).thenReturn(Optional.of(homeData));
triggerCameraEvents();
// No triggered event is expected, because the binding is just started (with existing events).
assertEquals(0, handler.getTriggerChannelCount());
home.setEvents(Arrays.asList(event1, event2));
triggerCameraEvents();
// 1 triggered event is expected, because there is 1 new event since binding start (outdoor / detected human).
assertEquals(1, handler.getTriggerChannelCount());
assertEquals(new StringType("outdoor"),
handler.getNAThingProperty(NetatmoBindingConstants.CHANNEL_WELCOME_EVENT_TYPE));
assertEquals("HUMAN", handler.getLastDetectedObject());
home.setEvents(Arrays.asList(event1, event2));
triggerCameraEvents();
// No new triggered event is expected, because there are still the same events as before the refresh.
assertEquals(1, handler.getTriggerChannelCount());
assertEquals(new StringType("outdoor"),
handler.getNAThingProperty(NetatmoBindingConstants.CHANNEL_WELCOME_EVENT_TYPE));
assertEquals("HUMAN", handler.getLastDetectedObject());
home.setEvents(Arrays.asList(event1, event2, event3));
triggerCameraEvents();
// 1 new triggered event is expected (2 in sum), because there is 1 new event since the last triggered event
// (movement after outdoor / detected human).
assertEquals(2, handler.getTriggerChannelCount());
assertEquals(new StringType("movement"),
handler.getNAThingProperty(NetatmoBindingConstants.CHANNEL_WELCOME_EVENT_TYPE));
assertEquals("MOVEMENT", handler.getLastDetectedObject());
}
@Test
public void testTriggerChannelIfRequiredNoEventAvailable() {
NAWelcomeHome home = new NAWelcomeHome();
home.setId(DUMMY_HOME_ID);
NAWelcomeHomeData homeData = new NAWelcomeHomeData();
homeData.setHomes(Collections.singletonList(home));
when(bridgeHandlerMock.getWelcomeDataBody(DUMMY_HOME_ID)).thenReturn(Optional.of(homeData));
triggerCameraEvents();
// No triggered event is expected, because there aren't any events (the collection is NULL)
assertEquals(0, handler.getTriggerChannelCount());
home.setEvents(Collections.emptyList());
triggerCameraEvents();
// No triggered event is expected, because there aren't any events (the collection is empty)
assertEquals(0, handler.getTriggerChannelCount());
}
@Test
public void testTriggerChannelIfRequiredPersonMovement() {
NAWelcomeHome home = initHome();
NAWelcomeEvent event = createEvent(1592661882, NAWebhookCameraEvent.EventTypeEnum.MOVEMENT);
event.setPersonId("1");
home.getEvents().add(event);
triggerCameraEvents();
assertEquals(1, handler.getTriggerChannelCount());
assertEquals(new StringType("movement"),
handler.getNAThingProperty(NetatmoBindingConstants.CHANNEL_WELCOME_EVENT_TYPE));
assertEquals("HUMAN", handler.getLastDetectedObject());
}
@Test
public void testTriggerChannelIfRequiredHumanMovement() {
NAWelcomeHome home = initHome();
NAWelcomeEvent event = createEvent(1592661882, NAWebhookCameraEvent.EventTypeEnum.MOVEMENT);
event.setCategory(NAWelcomeEvent.CategoryEnum.HUMAN);
home.getEvents().add(event);
triggerCameraEvents();
assertEquals(1, handler.getTriggerChannelCount());
assertEquals(new StringType("movement"),
handler.getNAThingProperty(NetatmoBindingConstants.CHANNEL_WELCOME_EVENT_TYPE));
assertEquals("HUMAN", handler.getLastDetectedObject());
}
@Test
public void testTriggerChannelIfRequiredAnimalMovement() {
NAWelcomeHome home = initHome();
NAWelcomeEvent event = createEvent(1592661882, NAWebhookCameraEvent.EventTypeEnum.MOVEMENT);
event.setCategory(NAWelcomeEvent.CategoryEnum.ANIMAL);
home.getEvents().add(event);
triggerCameraEvents();
assertEquals(1, handler.getTriggerChannelCount());
assertEquals(new StringType("movement"),
handler.getNAThingProperty(NetatmoBindingConstants.CHANNEL_WELCOME_EVENT_TYPE));
assertEquals("ANIMAL", handler.getLastDetectedObject());
}
@Test
public void testTriggerChannelIfRequiredVehicleMovement() {
NAWelcomeHome home = initHome();
NAWelcomeEvent event = createEvent(1592661882, NAWebhookCameraEvent.EventTypeEnum.MOVEMENT);
event.setCategory(NAWelcomeEvent.CategoryEnum.VEHICLE);
home.getEvents().add(event);
triggerCameraEvents();
assertEquals(1, handler.getTriggerChannelCount());
assertEquals(new StringType("movement"),
handler.getNAThingProperty(NetatmoBindingConstants.CHANNEL_WELCOME_EVENT_TYPE));
assertEquals("VEHICLE", handler.getLastDetectedObject());
}
@Test
public void testMatchDetectedObjectEnums() {
assertArrayEquals(
"The detected object enums aren't equal anymore, that could lead to a bug! Please check the usages!",
Arrays.stream(NAWelcomeEvent.CategoryEnum.values()).map(Enum::name).toArray(),
Arrays.stream(NAWelcomeSubEvent.TypeEnum.values()).map(Enum::name).toArray());
}
private NAWelcomeHome initHome() {
NAWelcomeEvent initLastEvent = createEvent(1592661881, NAWebhookCameraEvent.EventTypeEnum.MOVEMENT);
NAWelcomeHome home = new NAWelcomeHome();
home.setId(DUMMY_HOME_ID);
List<NAWelcomeEvent> events = new ArrayList<>();
events.add(initLastEvent);
home.setEvents(events);
NAWelcomeHomeData homeData = new NAWelcomeHomeData();
homeData.setHomes(Collections.singletonList(home));
when(bridgeHandlerMock.getWelcomeDataBody(DUMMY_HOME_ID)).thenReturn(Optional.of(homeData));
triggerCameraEvents();
return home;
}
private void triggerCameraEvents() {
handler.updateReadings();
handler.triggerChannelIfRequired(NetatmoBindingConstants.CHANNEL_CAMERA_EVENT);
}
private static NAWelcomeEvent createPresenceEvent(int eventTime, NAWelcomeSubEvent.TypeEnum detectedObjectType) {
NAWelcomeSubEvent subEvent = new NAWelcomeSubEvent();
subEvent.setTime(eventTime);
subEvent.setType(detectedObjectType);
NAWelcomeEvent event = createEvent(eventTime, NAWebhookCameraEvent.EventTypeEnum.OUTDOOR);
event.setEventList(Collections.singletonList(subEvent));
return event;
}
private static NAWelcomeEvent createEvent(int eventTime, NAWebhookCameraEvent.EventTypeEnum eventType) {
NAWelcomeEvent event = new NAWelcomeEvent();
event.setType(eventType.toString());
event.setTime(eventTime);
return event;
}
private class NAWelcomeHomeHandlerAccessible extends NAWelcomeHomeHandler {
private int triggerChannelCount;
private String lastDetectedObject;
private NAWelcomeHomeHandlerAccessible(Thing thing) {
super(thing, timeZoneProviderMock);
}
@Override
protected Optional<NetatmoBridgeHandler> getBridgeHandler() {
return Optional.of(bridgeHandlerMock);
}
@Override
protected String getId() {
return DUMMY_HOME_ID;
}
@Override
protected void triggerChannel(@NonNull String channelID, @NonNull String event) {
triggerChannelCount++;
lastDetectedObject = event;
super.triggerChannel(channelID, event);
}
private int getTriggerChannelCount() {
return triggerChannelCount;
}
public String getLastDetectedObject() {
return lastDetectedObject;
}
}
}