added migrated 2.x add-ons

Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
Kai Kreuzer
2020-09-21 03:37:19 +02:00
parent bbf1a7fd29
commit 6df6783b60
11662 changed files with 1302875 additions and 11 deletions
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="target/classes" path="src/main/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" path="target/generated-sources/swagger/src/main/java"/>
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
<attributes>
<attribute name="test" value="true"/>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>org.openhab.binding.netatmo</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
</natures>
</projectDescription>
@@ -0,0 +1,50 @@
This content is produced and maintained by the openHAB project.
* Project home: https://www.openhab.org
== Declared Project Licenses
This program and the accompanying materials are made available under the terms
of the Eclipse Public License 2.0 which is available at
https://www.eclipse.org/legal/epl-2.0/.
== Source Code
https://github.com/openhab/openhab-addons
== Third-party Content
commons-codec
* License: Apache 2.0 License
* Project; https://commons.apache.org/proper/commons-codec
* Source: https://commons.apache.org/proper/commons-codec
json
* License: JSON License
* Project: https://www.json.org
* Source: https://github.com/douglascrockford/JSON-java
okhttp
* License: Apache 2.0 License
* Project: https://square.github.io/okhttp
* Source: https://github.com/square/okhttp
okio
* License: Apache 2.0 License
* Project: https://square.github.io/okio/2.x/okio/jvm/okio
* Source: https://github.com/square/okio
oltu.oauth2
* License: Apache 2.0 License
* Project: https://oltu.apache.org
* Source: https://svn.apache.org/viewvc/oltu/trunk
netatmo-swagger-decl
* License: MIT License
* Project: https://dev.netatmo.com
* Source: https://github.com/cbornet/netatmo-swagger-decl
retrofit
* License: Apache 2.0 License
* Project: https://square.github.io/retrofit
* Source: https://github.com/square/retrofit
@@ -0,0 +1,835 @@
# Netatmo Binding
The Netatmo binding integrates the following Netatmo products:
- *Personal Weather Station*. Reports temperature, humidity, air pressure, carbon dioxide concentration in the air, as well as the ambient noise level.
- *Thermostat*. Reports ambient temperature, allow to check target temperature, consult and change furnace heating status.
- *Indoor Camera / Welcome*. Reports last event and persons at home, consult picture and video from event/camera.
- *Outdoor Camera / Presence*. Reports last event, consult picture and video from event/camera.
See http://www.netatmo.com/ for details on their product.
## Binding Configuration
The binding has the following configuration options:
| Parameter | Name | Description |
|---------------------|----------------------|-----------------------------------|
| backgroundDiscovery | Background Discovery | 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. |
Before setting up your 'Things', you will have to grant openHAB to access Netatmo API.
Here is the procedure:
### 1. Application Creation
Create an application at https://dev.netatmo.com/dev/createapp
The variables you will need to get to setup the binding are:
* `<CLIENT_ID>` Your client ID taken from your App at https://dev.netatmo.com/dev/listapps
* `<CLIENT_SECRET>` A token provided along with the `<CLIENT_ID>`.
* `<USERNAME>` The username you use to connect to the Netatmo API (usually your mail address).
* `<PASSWORD>` The password attached to the above username.
### 2. Bridge and Things Configuration
Once you will get needed informations from the Netatmo API, you will be able to configure bridge and things.
E.g.
```
Bridge netatmo:netatmoapi:home [ clientId="<CLIENT_ID>", clientSecret="<CLIENT_SECRET>", username = "<USERNAME>", password = "<PASSWORD>", readStation=true|false, readHealthyHomeCoach=true|false, readThermostat=true|false, readWelcome=true|false, readPresence=true|false] {
Thing NAMain inside [ id="aa:aa:aa:aa:aa:aa" ]
Thing NAModule1 outside [ id="yy:yy:yy:yy:yy:yy", parentId="aa:aa:aa:aa:aa:aa" ]
Thing NHC homecoach [ id="cc:cc:cc:cc:cc:cc", [refreshInterval=60000] ]
Thing NAPlug plugtherm [ id="bb:bb:bb:bb:bb:bb", [refreshInterval=60000] ]
Thing NATherm1 thermostat [ id="xx:xx:xx:xx:xx:xx", parentId="bb:bb:bb:bb:bb:bb" ]
Thing NAWelcomeHome home [ id="58yyacaaexxxebca99x999x", refreshInterval=600000 ]
Thing NACamera camera [ id="cc:cc:cc:cc:cc:cc", parentId="58yyacaaexxxebca99x999x" ]
Thing NOC presenceOutdoorCamera [ id="dd:dd:dd:dd:dd:dd", parentId="58yyacaaexxxebca99x999x" ]
Thing NAWelcomePerson sysadmin [ id="aaaaaaaa-bbbb-cccc-eeee-zzzzzzzzzzzz", parentId="58yyacaaexxxebca99x999x" ]
...
}
```
### Webhook
For Welcome or Presence Camera, Netatmo servers can send push notifications to the Netatmo Binding by using a callback URL.
The webhook URL is setup at bridge level using "Webhook Address" parameter.
You will define here public way to access your openHAB server:
```
http(s)://xx.yy.zz.ww:8080
```
Your Netatmo App will be configured automatically by the bridge to the endpoint :
```
http(s)://xx.yy.zz.ww:8080/netatmo/%id%/camera
```
where %id% is the id of your camera thing.
Please be aware of Netatmo own limits regarding webhook usage that lead to a 24h ban-time when webhook does not answer 5 times.
### Configure Things
The IDs for the modules can be extracted from the developer documentation on the netatmo site.
First login with your user.
Then some examples of the documentation contain the **real results** of your weather station.
In order to try the examples, you need the `device_id` of your Netatmo station.
You can find it in the configuration menu of the app (android or apple).
Get the IDs of your devices (indoor, outdoor, rain gauge)
[here](https://dev.netatmo.com/resources/technical/reference/weather/getstationsdata).
`main_device` is the ID of the "main device", the indoor sensor.
This is equal to the MAC address of the Netatmo.
The other modules you can recognize by "module_name" and then note the "\_id" which you need later.
**Another way to get the IDs is to calculate them:**
You have to calculate the ID for the outside module as follows: (it cannot be read from the app)
- if the first serial character is "h": start with "02"
- if the first serial character is "i": start with "03"
append ":00:00:",
split the rest into three parts of two characters and append with a colon as delimiter.
For example your serial number "h00bcdc" should end up as "02:00:00:00:bc:dc".
## Discovery
If you did not manually create things in the *.things file, the Netatmo Binding is able to discover automatically all depending modules and devices from Netatmo website.
## Channels
### Weather Station Main Indoor Device
Weather station does not need any refreshInterval setting.
Based on a standard update period of 10mn by Netatmo systems - it will auto adapt to stick closest as possible to last data availability.
Example item for the **indoor module**:
```
Number Netatmo_Indoor_CO2 "CO2" <carbondioxide> { channel = "netatmo:NAMain:home:inside:Co2" }
```
**Supported channels for the main indoor module:**
| Channel ID | Item Type | Description |
|---------------------|----------------------|----------------------------------------------------------|
| Co2 | Number:Dimensionless | Air quality |
| MinCo2 | Number:Dimensionless | Minimum CO2 on current day |
| MinCo2ThisWeek | Number:Dimensionless | Minimum CO2 this week |
| MinCo2ThisMonth | Number:Dimensionless | Minimum CO2 this month |
| MaxCo2 | Number:Dimensionless | Maximum CO2 on current day |
| MaxCo2ThisWeek | Number:Dimensionless | Maximum CO2 this week |
| MaxCo2ThisMonth | Number:Dimensionless | Maximum CO2 this month |
| DateMinCo2 | DateTime | Date when minimum CO2 was reached on current day |
| DateMinCo2ThisWeek | DateTime | Date when minimum CO2 was reached this week |
| DateMinCo2ThisMonth | DateTime | Date when minimum CO2 was reached this month |
| DateMaxCo2 | DateTime | Date when maximum CO2 was reached on current day |
| DateMaxCo2ThisWeek | DateTime | Date when maximum CO2 was reached this week |
| DateMaxCo2ThisMonth | DateTime | Date when maximum CO2 was reached this month |
| Temperature | Number:Temperature | Current temperature |
| TempTrend | String | Temperature evolution trend (up, down, stable) |
| Noise | Number:Dimensionless | Current noise level |
| MinNoise | Number:Dimensionless | Minimum noise on current day |
| MinNoiseThisWeek | Number:Dimensionless | Minimum noise this week |
| MinNoiseThisMonth | Number:Dimensionless | Minimum noise this month |
| MaxNoise | Number:Dimensionless | Maximum noise on current day |
| MaxNoiseThisWeek | Number:Dimensionless | Maximum noise this week |
| MaxNoiseThisMonth | Number:Dimensionless | Maximum noise this month |
| DateMinNoise | DateTime | Date when minimum noise was reached on current day |
| DateMinNoiseThisWeek| DateTime | Date when minimum noise was reached this week |
| DateMinNoiseThisMonth| DateTime | Date when minimum noise was reached this month |
| DateMaxNoise | DateTime | Date when maximum noise was reached on current day |
| DateMaxNoiseThisWeek| DateTime | Date when maximum noise was reached this week |
| DateMaxNoiseThisMonth| DateTime | Date when maximum noise was reached this month |
| Pressure | Number:Pressure | Current pressure |
| MinPressure | Number:Pressure | Minimum pressure on current day |
| MinPressureThisWeek | Number:Pressure | Minimum pressure this week |
| MinPressureThisMonth| Number:Pressure | Minimum pressure this month |
| MaxPressure | Number:Pressure | Maximum pressure on current day |
| MaxPressureThisWeek | Number:Pressure | Maximum pressure this week |
| MaxPressureThisMonth| Number:Pressure | Maximum pressure this month |
| DateMinPressure | DateTime | Date when minimum pressure was reached on current day |
| DateMinPressureThisWeek | DateTime | Date when minimum pressure was reached this week |
| DateMinPressureThisMonth| DateTime | Date when minimum pressure was reached this month |
| DateMaxPressure | DateTime | Date when maximum pressure was reached on current day |
| DateMaxPressureThisWeek | DateTime | Date when maximum pressure was reached this week |
| DateMaxPressureThisMonth| DateTime | Date when maximum pressure was reached this month |
| PressTrend | String | Pressure evolution trend for last 12h (up, down, stable) |
| AbsolutePressure | Number:Pressure | Absolute pressure |
| Humidity | Number:Dimensionless | Current humidity |
| MinHumidity | Number:Dimensionless | Minimum humidity on current day |
| MinHumidityThisWeek | Number:Dimensionless | Minimum humidity this week |
| MinHumidityThisMonth| Number:Dimensionless | Minimum humidity this month |
| MaxHumidity | Number:Dimensionless | Maximum humidity on current day |
| MaxHumidityThisWeek | Number:Dimensionless | Maximum humidity this week |
| MaxHumidityThisMonth| Number:Dimensionless | Maximum humidity this month |
| DateMinHumidity | DateTime | Date when minimum humidity was reached on current day |
| DateMinHumidityThisWeek | DateTime | Date when minimum humidity was reached this week |
| DateMinHumidityThisMonth| DateTime | Date when minimum humidity was reached this month |
| DateMaxHumidity | DateTime | Date when maximum humidity was reached on current day |
| DateMaxHumidityThisWeek | DateTime | Date when maximum humidity was reached this week |
| DateMaxHumidityThisMonth| DateTime | Date when maximum humidity was reached this month |
| Humidex | Number | Computed Humidex index |
| HeatIndex | Number:Temperature | Computed Heat Index |
| Dewpoint | Number:Temperature | Computed dewpoint temperature |
| DewpointDepression | Number:Temperature | Computed dewpoint depression |
| MinTemp | Number:Temperature | Minimum temperature on current day |
| MinTempThisWeek | Number:Temperature | Minimum temperature this week |
| MinTempThisMonth | Number:Temperature | Minimum temperature this month |
| MaxTemp | Number:Temperature | Maximum temperature on current day |
| MaxTempThisWeek | Number:Temperature | Maximum temperature this week |
| MaxTempThisMonth | Number:Temperature | Maximum temperature this month |
| DateMinTemp | DateTime | Date when minimum temperature was reached on current day |
| DateMinTempThisWeek | DateTime | Date when minimum temperature was reached this week |
| DateMinTempThisMonth| DateTime | Date when minimum temperature was reached this month |
| DateMaxTemp | DateTime | Date when maximum temperature was reached on current day |
| DateMaxTempThisWeek | DateTime | Date when maximum temperature was reached this week |
| DateMaxTempThisMonth| DateTime | Date when maximum temperature was reached this month |
| DateMinTemp | DateTime | Date when minimum temperature was reached on current day |
| DateMaxTemp | DateTime | Date when maximum temperature was reached on current day |
| TimeStamp | DateTime | Timestamp when data was measured |
| LastStatusStore | DateTime | Last status store |
| WifiStatus | Number | Signal strength (0 for no signal, 1 for weak, 2 for average, 3 for good or 4 for excellent) |
| Location | Location | Location of the device |
All these channels are read only.
### Weather Station Outdoor module
Example item for the **outdoor module**
```
Number Netatmo_Outdoor_Temperature "Temperature" { channel = "netatmo:NAModule1:home:outside:Temperature" }
```
**Supported channels for the outdoor module:**
| Channel ID | Item Type | Description |
|---------------------|----------------------|----------------------------------------------------------|
| Temperature | Number:Temperature | Current temperature |
| TempTrend | String | Temperature evolution trend (up, down, stable) |
| Humidity | Number:Dimensionless | Current humidity |
| MinHumidity | Number:Dimensionless | Minimum humidity on current day |
| MinHumidityThisWeek | Number:Dimensionless | Minimum humidity this week |
| MinHumidityThisMonth| Number:Dimensionless | Minimum humidity this month |
| MaxHumidity | Number:Dimensionless | Maximum humidity on current day |
| MaxHumidityThisWeek | Number:Dimensionless | Maximum humidity this week |
| MaxHumidityThisMonth| Number:Dimensionless | Maximum humidity this month |
| DateMinHumidity | DateTime | Date when minimum humidity was reached on current day |
| DateMinHumidityThisWeek | DateTime | Date when minimum humidity was reached this week |
| DateMinHumidityThisMonth| DateTime | Date when minimum humidity was reached this month |
| DateMaxHumidity | DateTime | Date when maximum humidity was reached on current day |
| DateMaxHumidityThisWeek | DateTime | Date when maximum humidity was reached this week |
| DateMaxHumidityThisMonth| DateTime | Date when maximum humidity was reached this month |
| Humidex | Number | Computed Humidex index |
| HeatIndex | Number:Temperature | Computed Heat Index |
| Dewpoint | Number:Temperature | Computed dewpoint temperature |
| DewpointDepression | Number:Temperature | Computed dewpoint depression |
| MinTemp | Number:Temperature | Minimum temperature on current day |
| MinTempThisWeek | Number:Temperature | Minimum temperature this week |
| MinTempThisMonth | Number:Temperature | Minimum temperature this month |
| MaxTemp | Number:Temperature | Maximum temperature on current day |
| MaxTempThisWeek | Number:Temperature | Maximum temperature this week |
| MaxTempThisMonth | Number:Temperature | Maximum temperature this month |
| DateMinTemp | DateTime | Date when minimum temperature was reached on current day |
| DateMinTempThisWeek | DateTime | Date when minimum temperature was reached this week |
| DateMinTempThisMonth| DateTime | Date when minimum temperature was reached this month |
| DateMaxTemp | DateTime | Date when maximum temperature was reached on current day |
| DateMaxTempThisWeek | DateTime | Date when maximum temperature was reached this week |
| DateMaxTempThisMonth| DateTime | Date when maximum temperature was reached this month |
| TimeStamp | DateTime | Timestamp when data was measured |
| LastMessage | DateTime | Last message emitted by the module |
| LowBattery | Switch | Low battery |
| BatteryVP | Number | Battery level |
| RfStatus | Number | Signal strength (0 for no signal, 1 for weak, 2 for average, 3 for good or 4 for excellent) |
All these channels are read only.
### Weather Station Additional Indoor module
Example item for the **indoor module**
```
Number Netatmo_Indoor2_Temperature "Temperature" { channel = "netatmo:NAModule4:home:insidesupp:Temperature" }
```
**Supported channels for the additional indoor module:**
| Channel ID | Item Type | Description |
|---------------------|----------------------|----------------------------------------------------------|
| Co2 | Number:Dimensionless | Air quality |
| MinCo2 | Number:Dimensionless | Minimum CO2 on current day |
| MinCo2ThisWeek | Number:Dimensionless | Minimum CO2 this week |
| MinCo2ThisMonth | Number:Dimensionless | Minimum CO2 this month |
| MaxCo2 | Number:Dimensionless | Maximum CO2 on current day |
| MaxCo2ThisWeek | Number:Dimensionless | Maximum CO2 this week |
| MaxCo2ThisMonth | Number:Dimensionless | Maximum CO2 this month |
| DateMinCo2 | DateTime | Date when minimum CO2 was reached on current day |
| DateMinCo2ThisWeek | DateTime | Date when minimum CO2 was reached this week |
| DateMinCo2ThisMonth | DateTime | Date when minimum CO2 was reached this month |
| DateMaxCo2 | DateTime | Date when maximum CO2 was reached on current day |
| DateMaxCo2ThisWeek | DateTime | Date when maximum CO2 was reached this week |
| DateMaxCo2ThisMonth | DateTime | Date when maximum CO2 was reached this month |
| Temperature | Number:Temperature | Current temperature |
| TempTrend | String | Temperature evolution trend (up, down, stable) |
| Humidity | Number:Dimensionless | Current humidity |
| MinHumidity | Number:Dimensionless | Minimum humidity on current day |
| MinHumidityThisWeek | Number:Dimensionless | Minimum humidity this week |
| MinHumidityThisMonth| Number:Dimensionless | Minimum humidity this month |
| MaxHumidity | Number:Dimensionless | Maximum humidity on current day |
| MaxHumidityThisWeek | Number:Dimensionless | Maximum humidity this week |
| MaxHumidityThisMonth| Number:Dimensionless | Maximum humidity this month |
| DateMinHumidity | DateTime | Date when minimum humidity was reached on current day |
| DateMinHumidityThisWeek | DateTime | Date when minimum humidity was reached this week |
| DateMinHumidityThisMonth| DateTime | Date when minimum humidity was reached this month |
| DateMaxHumidity | DateTime | Date when maximum humidity was reached on current day |
| DateMaxHumidityThisWeek | DateTime | Date when maximum humidity was reached this week |
| DateMaxHumidityThisMonth| DateTime | Date when maximum humidity was reached this month |
| Humidex | Number | Computed Humidex index |
| HeatIndex | Number:Temperature | Computed Heat Index |
| Dewpoint | Number:Temperature | Computed dewpoint temperature |
| DewpointDepression | Number:Temperature | Computed dewpoint depression |
| MinTemp | Number:Temperature | Minimum temperature on current day |
| MinTempThisWeek | Number:Temperature | Minimum temperature this week |
| MinTempThisMonth | Number:Temperature | Minimum temperature this month |
| MaxTemp | Number:Temperature | Maximum temperature on current day |
| MaxTempThisWeek | Number:Temperature | Maximum temperature this week |
| MaxTempThisMonth | Number:Temperature | Maximum temperature this month |
| DateMinTemp | DateTime | Date when minimum temperature was reached on current day |
| DateMinTempThisWeek | DateTime | Date when minimum temperature was reached this week |
| DateMinTempThisMonth| DateTime | Date when minimum temperature was reached this month |
| DateMaxTemp | DateTime | Date when maximum temperature was reached on current day |
| DateMaxTempThisWeek | DateTime | Date when maximum temperature was reached this week |
| DateMaxTempThisMonth| DateTime | Date when maximum temperature was reached this month |
| TimeStamp | DateTime | Timestamp when data was measured |
| LastMessage | DateTime | Last message emitted by the module |
| LowBattery | Switch | Low battery |
| BatteryVP | Number | Battery level |
| RfStatus | Number | Signal strength (0 for no signal, 1 for weak, 2 for average, 3 for good or 4 for excellent) |
All these channels are read only.
### Rain Gauge
Example item for the **rain gauge**
```
Number Netatmo_Rain_Current "Rain [%.1f mm]" { channel = "netatmo:NAModule3:home:rain:Rain" }
```
**Supported channels for the rain guage:**
| Channel ID | Item Type | Description |
|---------------------|---------------|----------------------------------------------------------|
| Rain | Number:Length | Quantity of water |
| SumRain1 | Number:Length | Quantity of water on last hour |
| SumRain24 | Number:Length | Quantity of water on last day |
| SumRainThisWeek | Number:Length | Quantity of water this week |
| SumRainThisMonth | Number:Length | Quantity of water this month |
| TimeStamp | DateTime | Timestamp when data was measured |
| LastMessage | DateTime | Last message emitted by the module |
| LowBattery | Switch | Low battery |
| BatteryVP | Number | Battery level |
| RfStatus | Number | Signal strength (0 for no signal, 1 for weak, 2 for average, 3 for good or 4 for excellent) |
All these channels are read only.
### Weather Station Wind module
Example item for the **wind module**:
```
Number Netatmo_Wind_Strength "Wind Strength [%.0f KPH]" { channel = "netatmo:NAModule2:home:wind:WindStrength" }
```
**Supported channels for the wind module:**
| Channel ID | Item Type | Description |
|---------------------|--------------|----------------------------------------------------------|
| WindAngle | Number:Angle | Current 5 minutes average wind direction |
| WindStrength | Number:Speed | Current 5 minutes average wind speed |
| GustAngle | Number:Angle | Direction of the last 5 minutes highest gust wind |
| GustStrength | Number:Speed | Speed of the last 5 minutes highest gust wind |
| TimeStamp | DateTime | Timestamp when data was measured |
| LastMessage | DateTime | Last message emitted by the module |
| LowBattery | Switch | Low battery |
| BatteryVP | Number | Battery level |
| RfStatus | Number | Signal strength (0 for no signal, 1 for weak, 2 for average, 3 for good or 4 for excellent) |
| MaxWindStrength | Number:Speed | Maximum wind strength recorded |
| DateMaxWindStrength | DateTime | Timestamp when MaxWindStrength was recorded |
All these channels are read only.
### Healthy Home Coach Device
Example item for the **Healthy Home Coach**:
```
String Netatmo_LivingRoom_HomeCoach_HealthIndex "Climate" { channel = "netatmo:NHC:home:livingroom:HealthIndex" }
```
**Supported channels for the healthy home coach device:**
| Channel ID | Item Type | Description |
|---------------------|----------------------|----------------------------------------------------------|
| HealthIndex | String | Health index (healthy, fine, fair, poor, unhealthy) |
| Co2 | Number:Dimensionless | Air quality |
| Temperature | Number:Temperature | Current temperature |
| TempTrend | String | Temperature evolution trend (up, down, stable) |
| Noise | Number:Dimensionless | Current noise level |
| Pressure | Number:Pressure | Current pressure |
| PressTrend | String | Pressure evolution trend for last 12h (up, down, stable) |
| AbsolutePressure | Number:Pressure | Absolute pressure |
| Humidity | Number:Dimensionless | Current humidity |
| MinTemp | Number:Temperature | Minimum temperature on current day |
| MaxTemp | Number:Temperature | Maximum temperature on current day |
| DateMinTemp | DateTime | Date when minimum temperature was reached on current day |
| DateMaxTemp | DateTime | Date when maximum temperature was reached on current day |
| TimeStamp | DateTime | Timestamp when data was measured |
| LastStatusStore | DateTime | Last status store |
| WifiStatus | Number | Signal strength (0 for no signal, 1 for weak, 2 for average, 3 for good or 4 for excellent) |
| Location | Location | Location of the device |
All these channels are read only.
### Thermostat Relay Device
**Supported channels for the thermostat relay device:**
| Channel ID | Item Type | Description |
|---------------------|-----------|----------------------------------------------------------|
| ConnectedBoiler | Switch | Plug connected boiler |
| LastPlugSeen | DateTime | Last plug seen |
| LastBilan | DateTime | Month of the last available thermostat bilan |
| LastStatusStore | DateTime | Last status store |
| WifiStatus | Number | Signal strength (0 for no signal, 1 for weak, 2 for average, 3 for good or 4 for excellent) |
| Location | Location | Location of the device |
All these channels are read only.
### Thermostat Module
**Supported channels for the thermostat module:**
| Channel ID | Item Type | Description |
|---------------------|--------------------|------------------------------------------------------------|
| Temperature | Number:Temperature | Current temperature |
| Sp_Temperature | Number:Temperature | Thermostat temperature setpoint |
| SetpointMode | String | Chosen setpoint_mode (program, away, hg, manual, off, max) |
| Planning | String | Id of the currently active planning when mode = program |
| ThermRelayCmd | Switch | Indicates whether the furnace is heating or not |
| ThermOrientation | Number | Physical orientation of the thermostat module |
| TimeStamp | DateTime | Timestamp when data was measured |
| SetpointEndTime | DateTime | Thermostat goes back to schedule after that timestamp |
| LastMessage | DateTime | Last message emitted by the module |
| LowBattery | Switch | Low battery |
| BatteryVP | Number | Battery level |
| RfStatus | Number | Signal strength (0 for no signal, 1 for weak, 2 for average, 3 for good or 4 for excellent) |
All these channels except Sp_Temperature, SetpointMode and Planning are read only.
### Welcome Home
All these channels are read only.
**Supported channels for the Home thing:**
| Channel ID | Item Type | Description |
|--------------------------|-----------|----------------------------------------------------------|
| welcomeHomeCity | String | City of the home |
| welcomeHomeCountry | String | Country of the home |
| welcomeHomeTimezone | String | Timezone of the home |
| welcomeHomePersonCount | Number | Total number of Persons that are at home |
| welcomeHomeUnknownCount | Number | Count how many Unknown Persons are at home |
| welcomeEventType | String | Type of event |
| welcomeEventTime | DateTime | Time of occurrence of event |
| welcomeEventCameraId | String | Camera that detected the event |
| welcomeEventPersonId | String | Id of the person the event is about (if any) |
| welcomeEventSnapshot | Image | picture of the last event, if it applies |
| welcomeEventSnapshotURL | String | if the last event (depending upon event type) in the home lead a snapshot picture, the picture URL will be available here |
| welcomeEventVideoURL | String | if the last event (depending upon event type) in the home lead a snapshot picture, the corresponding video URL will be available here |
| welcomeEventVideoStatus | String | Status of the video (recording, deleted or available) |
| welcomeEventIsArrival | Switch | If person was considered "away" before being seen during this event |
| welcomeEventMessage | String | Message sent by Netatmo corresponding to given event |
| welcomeEventSubType | String | Sub-type of SD and Alim events |
**Supported trigger channels for the Home thing:**
| Channel Type ID | Options | Description |
|------------------|------------------------|-------------------------------------------------------|
| cameraEvent | | A camera event is triggered with a short delay but without requiring a webhook. The information of the event can get retrieved from the other "welcomeEvent" home thing channels |
| | HUMAN | Triggered when a human (or person) was detected |
| | ANIMAL | Triggered when an animal was detected |
| | MOVEMENT | Triggered when an unspecified movement was detected |
| | VEHICLE | Triggered when a vehicle was detected |
| welcomeHomeEvent | | A welcome home event is triggered directly via a configured webhook |
| | PERSON | Triggered when a concrete person was detected |
| | PERSON_AWAY | Triggered when a concrete person leaves |
| | MOVEMENT | Triggered when a movement was detected |
| | CONNECTION | Triggered when a camera connection gets created |
| | DISCONNECTION | Triggered when a camera connection got lost |
| | ON | Triggered when camera monitoring is switched on |
| | OFF | Triggered when camera monitoring is switched off |
| | BOOT | Triggered when a camera is booting |
| | SD | Triggered when a camera SD card status was changed |
| | ALIM | Triggered when a power supply status was changed |
| | NEW_MODULE | Triggered when a new module was discovered |
| | MODULE_CONNECT | Triggered when a module gets connected |
| | MODULE_DISCONNECT | Triggered when a module gets disconnected |
| | MODULE_LOW_BATTERY | Triggered when the battery of a module gets low |
| | MODULE_END_UPDATE | Triggered when a firmware update of a module is done |
| | TAG_BIG_MOVE | Triggered when a big movement of a tag was detected |
| | TAG_SMALL_MOVE | Triggered when a small movement of a tag was detected |
| | TAG_UNINSTALLED | Triggered when a tag gets uninstalled |
| | TAG_OPEN | Triggered when an open event of a tag was detected |
### Welcome and Presence Camera
Warnings:
- The URL of the live snapshot is a fixed URL so the value of the channel cameraLivePictureUrl / welcomeCameraLivePictureUrl will never be updated once first set by the binding. So to get a refreshed picture, you need to use the refresh parameter in your sitemap image element.
- Some features like the video surveillance are accessed via the local network, so it may be helpful to set a static IP address for the camera within your local network.
**Supported channels for the Welcome Camera thing:**
| Channel ID | Item Type | Read/Write | Description |
|-----------------------------|-----------|------------|--------------------------------------------------------------|
| welcomeCameraStatus | Switch | Read-write | State of the camera (video surveillance on/off) |
| welcomeCameraSdStatus | Switch | Read-only | State of the SD card |
| welcomeCameraAlimStatus | Switch | Read-only | State of the power connector |
| welcomeCameraIsLocal | Switch | Read-only | indicates whether the camera is on the same network than the openHAB Netatmo Binding |
| welcomeCameraLivePicture | Image | Read-only | Camera Live Snapshot |
| welcomeCameraLivePictureUrl | String | Read-only | Url of the live snapshot for this camera |
| welcomeCameraLiveStreamUrl | String | Read-only | Url of the live stream for this camera |
**Supported channels for the Presence Camera thing:**
Warnings:
- The floodlight auto-mode (cameraFloodlightAutoMode) isn't updated it is changed by another application. Therefore the binding handles its own state of the auto-mode. This has the advantage that the user can define its own floodlight switch off behaviour.
| Channel ID | Item Type | Read/Write | Description |
|-----------------------------|-----------|------------|--------------------------------------------------------------|
| cameraStatus | Switch | Read-write | State of the camera (video surveillance on/off) |
| cameraSdStatus | Switch | Read-only | State of the SD card |
| cameraAlimStatus | Switch | Read-only | State of the power connector |
| cameraIsLocal | Switch | Read-only | indicates whether the camera is on the same network than the openHAB Netatmo Binding |
| cameraLivePicture | Image | Read-only | Camera Live Snapshot |
| cameraLivePictureUrl | String | Read-only | Url of the live snapshot for this camera |
| cameraLiveStreamUrl | String | Read-only | Url of the live stream for this camera |
| cameraFloodlightAutoMode | Switch | Read-write | When set the floodlight gets switched to auto instead of off |
| cameraFloodlight | Switch | Read-write | Switch for the floodlight |
### Welcome Person
Netatmo API distinguishes two kinds of persons:
* Known persons : have been identified by the camera and you have defined a name for those.
* Unknown persons : identified by the camera, but no name defined.
Person things are automatically created in discovery process for all known persons.
**Supported channels for the Person thing:**
| Channel ID | Item Type | Description |
|-------------------------------|-----------|--------------------------------------------------------|
| welcomePersonLastSeen | DateTime | Time when this person was last seen |
| welcomePersonAtHome | Switch | Indicates if this person is known to be at home or not |
| welcomePersonAvatarUrl | String | URL for the avatar of this person |
| welcomePersonAvatar | Image | Avatar of this person |
| welcomePersonLastEventMessage | String | Last event message from this person |
| welcomePersonLastEventTime | DateTime | Last event message time for this person |
| welcomePersonLastEventUrl | String | URL for the picture of the last event for this person |
| welcomePersonLastEvent | Image | Picture of the last event for this person |
All these channels except welcomePersonAtHome are read only.
# Configuration Examples
## things/netatmo.things
```
// Bridge configuration:
Bridge netatmo:netatmoapi:home "Netatmo API" [ clientId="*********", clientSecret="**********", username = "mail@example.com", password = "******", readStation=true, readThermostat=false] {
// Thing configuration:
Thing NAMain inside "Netatmo Inside" [ id="aa:aa:aa:aa:aa:aa" ]
Thing NAModule1 outside "Netatmo Outside" [ id="bb:bb:bb:bb:bb:bb", parentId="aa:aa:aa:aa:aa:aa" ]
Thing NAModule3 rain "Netatmo Rain" [ id="cc:cc:cc:cc:cc:cc", parentId="aa:aa:aa:aa:aa:aa" ]
}
```
## items/netatmo.items
```
# Indoor Module
Number:Temperature Indoor_Temp "Temperature [%.1f %unit%]" <temperature> { channel = "netatmo:NAMain:home:inside:Temperature" }
Number:Temperature Indoor_Min_Temp "Min Temperature Today [%.1f %unit%]" <temperature> { channel = "netatmo:NAMain:home:inside:MinTemp" }
Number:Temperature Indoor_Min_Temp_This_Week "Min Temperature This Week [%.1f %unit%]" <temperature> { channel = "netatmo:NAMain:home:inside:MinTempThisWeek" }
Number:Temperature Indoor_Min_Temp_This_Month "Min Temperature This Month [%.1f %unit%]" <temperature> { channel = "netatmo:NAMain:home:inside:MinTempThisMonth" }
Number:Temperature Indoor_Max_Temp "Max Temperature Today [%.1f %unit%]" <temperature> { channel = "netatmo:NAMain:home:inside:MaxTemp" }
Number:Temperature Indoor_Max_Temp_This_Week "Max Temperature This Week [%.1f %unit%]" <temperature> { channel = "netatmo:NAMain:home:inside:MaxTempThisWeek" }
Number:Temperature Indoor_Max_Temp_This_Month "Max Temperature This Month [%.1f %unit%]" <temperature> { channel = "netatmo:NAMain:home:inside:MaxTempThisMonth" }
DateTime Indoor_Min_Temp_TS "Min Temperature Today [%1$td.%1$tm.%1$tY %1$tH:%1$tM]" <calendar> { channel = "netatmo:NAMain:home:inside:DateMinTemp" }
DateTime Indoor_Min_Temp_This_Week_TS "Min Temperature This Week [%1$td.%1$tm.%1$tY %1$tH:%1$tM]" <calendar> { channel = "netatmo:NAMain:home:inside:DateMinTempThisWeek" }
DateTime Indoor_Min_Temp_This_Month_TS "Min Temperature This Month [%1$td.%1$tm.%1$tY %1$tH:%1$tM]" <calendar> { channel = "netatmo:NAMain:home:inside:DateMinTempThisMonth" }
DateTime Indoor_Max_Temp_TS "Max Temperature Today [%1$td.%1$tm.%1$tY %1$tH:%1$tM]" <calendar> { channel = "netatmo:NAMain:home:inside:DateMaxTemp" }
DateTime Indoor_Max_Temp_This_Week_TS "Max Temperature This Week [%1$td.%1$tm.%1$tY %1$tH:%1$tM]" <calendar> { channel = "netatmo:NAMain:home:inside:DateMaxTempThisWeek" }
DateTime Indoor_Max_Temp_This_Month_TS "Max Temperature This Month [%1$td.%1$tm.%1$tY %1$tH:%1$tM]" <calendar> { channel = "netatmo:NAMain:home:inside:DateMaxTempThisMonth" }
Number:Dimensionless Indoor_Humidity "Humidity [%d %unit%]" <humidity> { channel = "netatmo:NAMain:home:inside:Humidity" }
Number:Dimensionless Indoor_Min_Humidity "Min Humidity Today [%d %unit%]" <humidity> { channel = "netatmo:NAMain:home:inside:MinHumidity" }
Number:Dimensionless Indoor_Min_Humidity_This_Week "Min Humidity This Week [%d %unit%]" <humidity> { channel = "netatmo:NAMain:home:inside:MinHumidityThisWeek" }
Number:Dimensionless Indoor_Min_Humidity_This_Month "Min Humidity This Month [%d %unit%]" <humidity> { channel = "netatmo:NAMain:home:inside:MinHumidityThisMonth" }
Number:Dimensionless Indoor_Max_Humidity "Max Humidity Today [%d %unit%]" <humidity> { channel = "netatmo:NAMain:home:inside:MaxHumidity" }
Number:Dimensionless Indoor_Max_Humidity_This_Week "Max Humidity This Week [%d %unit%]" <humidity> { channel = "netatmo:NAMain:home:inside:MaxHumidityThisWeek" }
Number:Dimensionless Indoor_Max_Humidity_This_Month "Max Humidity This Month [%d %unit%]" <humidity> { channel = "netatmo:NAMain:home:inside:MaxHumidityThisMonth" }
DateTime Indoor_Min_Humidity_TS "Min Humidity Today [%1$td.%1$tm.%1$tY %1$tH:%1$tM]" <calendar> { channel = "netatmo:NAMain:home:inside:DateMinHumidity" }
DateTime Indoor_Min_Humidity_This_Week_TS "Min Humidity This Week [%1$td.%1$tm.%1$tY %1$tH:%1$tM]" <calendar> { channel = "netatmo:NAMain:home:inside:DateMinHumidityThisWeek" }
DateTime Indoor_Min_Humidity_This_Month_TS "Min Humidity This Month [%1$td.%1$tm.%1$tY %1$tH:%1$tM]" <calendar> { channel = "netatmo:NAMain:home:inside:DateMinHumidityThisMonth" }
DateTime Indoor_Max_Humidity_TS "Max Humidity Today [%1$td.%1$tm.%1$tY %1$tH:%1$tM]" <calendar> { channel = "netatmo:NAMain:home:inside:DateMaxHumidity" }
DateTime Indoor_Max_Humidity_This_Week_TS "Max Humidity This Week [%1$td.%1$tm.%1$tY %1$tH:%1$tM]" <calendar> { channel = "netatmo:NAMain:home:inside:DateMaxHumidityThisWeek" }
DateTime Indoor_Max_Humidity_This_Month_TS "Max Humidity This Month [%1$td.%1$tm.%1$tY %1$tH:%1$tM]" <calendar> { channel = "netatmo:NAMain:home:inside:DateMaxHumidityThisMonth" }
Number Indoor_Humidex "Humidex [%.0f]" <temperature_hot> { channel = "netatmo:NAMain:home:inside:Humidex" }
Number:Temperature Indoor_HeatIndex "HeatIndex [%.1f %unit%]" <temperature_hot> { channel = "netatmo:NAMain:home:inside:HeatIndex" }
Number:Temperature Indoor_Dewpoint "Dewpoint [%.1f %unit%]" <temperature_cold> { channel = "netatmo:NAMain:home:inside:Dewpoint" }
Number:Temperature Indoor_DewpointDepression "DewpointDepression [%.1f %unit%]" <temperature_cold> { channel = "netatmo:NAMain:home:inside:DewpointDepression" }
Number:Dimensionless Indoor_Co2 "CO2 [%d %unit%]" <carbondioxide> { channel = "netatmo:NAMain:home:inside:Co2" }
Number:Dimensionless Indoor_Min_Co2 "Min CO2 Today [%.1f %unit%]" <carbondioxide> { channel = "netatmo:NAMain:home:inside:MinCo2" }
Number:Dimensionless Indoor_Min_Co2_This_Week "Min CO2 This Week [%.1f %unit%]" <carbondioxide> { channel = "netatmo:NAMain:home:inside:MinCo2ThisWeek" }
Number:Dimensionless Indoor_Min_Co2_This_Month "Min CO2 This Month [%.1f %unit%]" <carbondioxide> { channel = "netatmo:NAMain:home:inside:MinCo2ThisMonth" }
Number:Dimensionless Indoor_Max_Co2 "Max CO2 Today [%.1f %unit%]" <carbondioxide> { channel = "netatmo:NAMain:home:inside:MaxCo2" }
Number:Dimensionless Indoor_Max_Co2_This_Week "Max CO2 This Week [%.1f %unit%]" <carbondioxide> { channel = "netatmo:NAMain:home:inside:MaxCo2ThisWeek" }
Number:Dimensionless Indoor_Max_Co2_This_Month "Max CO2 This Month [%.1f %unit%]" <carbondioxide> { channel = "netatmo:NAMain:home:inside:MaxCo2ThisMonth" }
DateTime Indoor_Min_Co2_TS "Min CO2 Today [%1$td.%1$tm.%1$tY %1$tH:%1$tM]" <calendar> { channel = "netatmo:NAMain:home:inside:DateMinCo2" }
DateTime Indoor_Min_Co2_This_Week_TS "Min CO2 This Week [%1$td.%1$tm.%1$tY %1$tH:%1$tM]" <calendar> { channel = "netatmo:NAMain:home:inside:DateMinCo2ThisWeek" }
DateTime Indoor_Min_Co2_This_Month_TS "Min CO2 This Month [%1$td.%1$tm.%1$tY %1$tH:%1$tM]" <calendar> { channel = "netatmo:NAMain:home:inside:DateMinCo2ThisMonth" }
DateTime Indoor_Max_Co2_TS "Max CO2 Today [%1$td.%1$tm.%1$tY %1$tH:%1$tM]" <calendar> { channel = "netatmo:NAMain:home:inside:DateMaxCo2" }
DateTime Indoor_Max_Co2_This_Week_TS "Max CO2 This Week [%1$td.%1$tm.%1$tY %1$tH:%1$tM]" <calendar> { channel = "netatmo:NAMain:home:inside:DateMaxCo2ThisWeek" }
DateTime Indoor_Max_Co2_This_Month_TS "Max CO2 This Month [%1$td.%1$tm.%1$tY %1$tH:%1$tM]" <calendar> { channel = "netatmo:NAMain:home:inside:DateMaxCo2ThisMonth" }
Number:Pressure Indoor_Pressure "Pressure [%.1f %unit%]" <pressure> { channel = "netatmo:NAMain:home:inside:Pressure" }
Number:Pressure Indoor_Min_Pressure "Min Pressure Today [%d %unit%]" <pressure> { channel = "netatmo:NAMain:home:inside:MinPressure" }
Number:Pressure Indoor_Min_Pressure_This_Week "Min Pressure This Week [%d %unit%]" <pressure> { channel = "netatmo:NAMain:home:inside:MinPressureThisWeek" }
Number:Pressure Indoor_Min_Pressure_This_Month "Min Pressure This Month [%d %unit%]" <pressure> { channel = "netatmo:NAMain:home:inside:MinPressureThisMonth" }
Number:Pressure Indoor_Max_Pressure "Max Pressure Today [%d %unit%]" <pressure> { channel = "netatmo:NAMain:home:inside:MaxPressure" }
Number:Pressure Indoor_Max_Pressure_This_Week "Max Pressure This Week [%d %unit%]" <pressure> { channel = "netatmo:NAMain:home:inside:MaxPressureThisWeek" }
Number:Pressure Indoor_Max_Pressure_This_Month "Max Pressure This Month [%d %unit%]" <pressure> { channel = "netatmo:NAMain:home:inside:MaxPressureThisMonth" }
DateTime Indoor_Min_Pressure_TS "Min Pressure Today [%1$td.%1$tm.%1$tY %1$tH:%1$tM]" <calendar> { channel = "netatmo:NAMain:home:inside:DateMinPressure" }
DateTime Indoor_Min_Pressure_This_Week_TS "Min Pressure This Week [%1$td.%1$tm.%1$tY %1$tH:%1$tM]" <calendar> { channel = "netatmo:NAMain:home:inside:DateMinPressureThisWeek" }
DateTime Indoor_Min_Pressure_This_Month_TS "Min Pressure This Month [%1$td.%1$tm.%1$tY %1$tH:%1$tM]" <calendar> { channel = "netatmo:NAMain:home:inside:DateMinPressureThisMonth" }
DateTime Indoor_Max_Pressure_TS "Max Pressure Today [%1$td.%1$tm.%1$tY %1$tH:%1$tM]" <calendar> { channel = "netatmo:NAMain:home:inside:DateMaxPressure" }
DateTime Indoor_Max_Pressure_This_Week_TS "Max Pressure This Week [%1$td.%1$tm.%1$tY %1$tH:%1$tM]" <calendar> { channel = "netatmo:NAMain:home:inside:DateMaxPressureThisWeek" }
DateTime Indoor_Max_Pressure_This_Month_TS "Max Pressure This Month [%1$td.%1$tm.%1$tY %1$tH:%1$tM]" <calendar> { channel = "netatmo:NAMain:home:inside:DateMaxPressureThisMonth" }
Number:Pressure Indoor_AbsolutePressure "AbsolutePressure [%.1f %unit%]" <pressure> { channel = "netatmo:NAMain:home:inside:AbsolutePressure" }
Number:Dimensionless Indoor_Noise "Noise [%d %unit%]" <soundvolume> { channel = "netatmo:NAMain:home:inside:Noise" }
Number:Dimensionless Indoor_Min_Noise "Min Noise Today [%.1f %unit%]" <soundvolume> { channel = "netatmo:NAMain:home:inside:MinNoise" }
Number:Dimensionless Indoor_Min_Noise_This_Week "Min Noise This Week [%.1f %unit%]" <soundvolume> { channel = "netatmo:NAMain:home:inside:MinNoiseThisWeek" }
Number:Dimensionless Indoor_Min_Noise_This_Month "Min Noise This Month [%.1f %unit%]" <soundvolume> { channel = "netatmo:NAMain:home:inside:MinNoiseThisMonth" }
Number:Dimensionless Indoor_Max_Noise "Max Noise Today [%.1f %unit%]" <soundvolume> { channel = "netatmo:NAMain:home:inside:MaxNoise" }
Number:Dimensionless Indoor_Max_Noise_This_Week "Max Noise This Week [%.1f %unit%]" <soundvolume> { channel = "netatmo:NAMain:home:inside:MaxNoiseThisWeek" }
Number:Dimensionless Indoor_Max_Noise_This_Month "Max Noise This Month [%.1f %unit%]" <soundvolume> { channel = "netatmo:NAMain:home:inside:MaxNoiseThisMonth" }
DateTime Indoor_Min_Noise_TS "Min Noise Today [%1$td.%1$tm.%1$tY %1$tH:%1$tM]" <calendar> { channel = "netatmo:NAMain:home:inside:DateMinNoise" }
DateTime Indoor_Min_Noise_This_Week_TS "Min Noise This Week [%1$td.%1$tm.%1$tY %1$tH:%1$tM]" <calendar> { channel = "netatmo:NAMain:home:inside:DateMinNoiseThisWeek" }
DateTime Indoor_Min_Noise_This_Month_TS "Min Noise This Month [%1$td.%1$tm.%1$tY %1$tH:%1$tM]" <calendar> { channel = "netatmo:NAMain:home:inside:DateMinNoiseThisMonth" }
DateTime Indoor_Max_Noise_TS "Max Noise Today [%1$td.%1$tm.%1$tY %1$tH:%1$tM]" <calendar> { channel = "netatmo:NAMain:home:inside:DateMaxNoise" }
DateTime Indoor_Max_Noise_This_Week_TS "Max Noise This Week [%1$td.%1$tm.%1$tY %1$tH:%1$tM]" <calendar> { channel = "netatmo:NAMain:home:inside:DateMaxNoiseThisWeek" }
DateTime Indoor_Max_Noise_This_Month_TS "Max Noise This Month [%1$td.%1$tm.%1$tY %1$tH:%1$tM]" <calendar> { channel = "netatmo:NAMain:home:inside:DateMaxNoiseThisMonth" }
Number Indoor_WifiStatus "WifiStatus [%s]" <signal> { channel = "netatmo:NAMain:home:inside:WifiStatus" }
DateTime Indoor_TimeStamp "TimeStamp [%1$td.%1$tm.%1$tY %1$tH:%1$tM]" <calendar> { channel = "netatmo:NAMain:home:inside:TimeStamp" }
Location Indoor_Location "Location" <movecontrol> { channel = "netatmo:NAMain:home:inside:Location" }
DateTime Indoor_LastStatusStore "LastStatusStore [%1$td.%1$tm.%1$tY %1$tH:%1$tM]" <text> { channel = "netatmo:NAMain:home:inside:LastStatusStore" }
# Outdoor Module
Number:Temperature Outdoor_Temperature "Temperature [%.1f %unit%]" <temperature> { channel = "netatmo:NAModule1:home:outside:Temperature" }
String Outdoor_TempTrend "TempTrend [%s]" <line> { channel = "netatmo:NAModule1:home:outside:TempTrend" }
Number:Dimensionless Outdoor_Humidity "Humidity [%d %unit%]" <humidity> { channel = "netatmo:NAModule1:home:outside:Humidity" }
Number Outdoor_Humidex "Humidex [%.0f]" <temperature_hot> { channel = "netatmo:NAModule1:home:outside:Humidex" }
Number:Temperature Outdoor_HeatIndex "HeatIndex [%.1f %unit%]" <temperature_hot> { channel = "netatmo:NAModule1:home:outside:HeatIndex" }
Number:Temperature Outdoor_Dewpoint "Dewpoint [%.1f %unit%]" <temperature_cold> { channel = "netatmo:NAModule1:home:outside:Dewpoint" }
Number:Temperature Outdoor_DewpointDepression "DewpointDepression [%.1f %unit%]" <temperature_cold> { channel = "netatmo:NAModule1:home:outside:DewpointDepression" }
Number Outdoor_RfStatus "RfStatus [%.0f / 5]" <signal> { channel = "netatmo:NAModule1:home:outside:RfStatus" }
Switch Outdoor_LowBattery "LowBattery [%s]" <siren> { channel = "netatmo:NAModule1:home:outside:LowBattery" }
Number Outdoor_BatteryVP "BatteryVP [%.0f %%]" <battery> { channel = "netatmo:NAModule1:home:outside:BatteryVP" }
DateTime Outdoor_TimeStamp "TimeStamp [%1$td.%1$tm.%1$tY %1$tH:%1$tM]" <calendar> { channel = "netatmo:NAModule1:home:outside:TimeStamp" }
DateTime Outdoor_LastMessage "LastMessage [%1$td.%1$tm.%1$tY %1$tH:%1$tM]" <text> { channel = "netatmo:NAModule1:home:outside:LastMessage" }
# Rain Module
Number:Length Rain_Hour "Rain Last Hour [%.02f %unit%]" <rain> {channel="netatmo:NAModule3:home:rain:SumRain1"}
Number:Length Rain_Today "Rain Today [%.02f %unit%]" <rain> {channel="netatmo:NAModule3:home:rain:SumRain24"}
Number:Length Rain_Week "Rain This Week [%.02f %unit%]" <rain> {channel="netatmo:NAModule3:home:rain:SumRainThisWeek"}
Number:Length Rain_Month "Rain This Month [%.02f %unit%]" <rain> {channel="netatmo:NAModule3:home:rain:SumRainThisMonth"}
Number Rain_BatteryVP "Rain battery status [%d%%]" <battery> {channel="netatmo:NAModule3:home:rain:BatteryVP"}
```
## sitemaps/netatmo.sitemap
```
sitemap netatmo label="Netatmo" {
Frame label="Indoor" {
Text item=Indoor_Temp
Text item=Indoor_Min_Temp
Text item=Indoor_Min_Temp_This_Week
Text item=Indoor_Min_Temp_This_Month
Text item=Indoor_Max_Temp
Text item=Indoor_Max_Temp_This_Week
Text item=Indoor_Max_Temp_This_Month
Text item=Indoor_Min_Temp_TS
Text item=Indoor_Min_Temp_This_Week_TS
Text item=Indoor_Min_Temp_This_Month_TS
Text item=Indoor_Max_Temp_TS
Text item=Indoor_Max_Temp_This_Week_TS
Text item=Indoor_Max_Temp_This_Month_TS
Text item=Indoor_Humidity
Text item=Indoor_Min_Humidity
Text item=Indoor_Min_Humidity_This_Week
Text item=Indoor_Min_Humidity_This_Month
Text item=Indoor_Max_Humidity
Text item=Indoor_Max_Humidity_This_Week
Text item=Indoor_Max_Humidity_This_Month
Text item=Indoor_Min_Humidity_TS
Text item=Indoor_Min_Humidity_This_Week_TS
Text item=Indoor_Min_Humidity_This_Month_TS
Text item=Indoor_Max_Humidity_TS
Text item=Indoor_Max_Humidity_This_Week_TS
Text item=Indoor_Max_Humidity_This_Month_TS
Text item=Indoor_Humidex valuecolor=[<20.1="green",<29.1="blue",<28.1="yellow",<45.1="orange",<54.1="red",>54.1="maroon"]
Text item=Indoor_HeatIndex
Text item=Indoor_Dewpoint
Text item=Indoor_DewpointDepression
Text item=Indoor_Co2 valuecolor=[<800="green",<1000="orange",<1400="red",>1399="maroon"]
Text item=Indoor_Min_Co2 valuecolor=[<800="green",<1000="orange",<1400="red",>1399="maroon"]
Text item=Indoor_Min_Co2_This_Week valuecolor=[<800="green",<1000="orange",<1400="red",>1399="maroon"]
Text item=Indoor_Min_Co2_This_Month valuecolor=[<800="green",<1000="orange",<1400="red",>1399="maroon"]
Text item=Indoor_Max_Co2 valuecolor=[<800="green",<1000="orange",<1400="red",>1399="maroon"]
Text item=Indoor_Max_Co2_This_Week valuecolor=[<800="green",<1000="orange",<1400="red",>1399="maroon"]
Text item=Indoor_Max_Co2_This_Month valuecolor=[<800="green",<1000="orange",<1400="red",>1399="maroon"]
Text item=Indoor_Min_Co2_TS
Text item=Indoor_Min_Co2_This_Week_TS
Text item=Indoor_Min_Co2_This_Month_TS
Text item=Indoor_Max_Co2_TS
Text item=Indoor_Max_Co2_This_Week_TS
Text item=Indoor_Max_Co2_This_Month_TS
Text item=Indoor_Pressure
Text item=Indoor_Min_Pressure
Text item=Indoor_Min_Pressure_This_Week
Text item=Indoor_Min_Pressure_This_Month
Text item=Indoor_Max_Pressure
Text item=Indoor_Max_Pressure_This_Week
Text item=Indoor_Max_Pressure_This_Month
Text item=Indoor_Min_Pressure_TS
Text item=Indoor_Min_Pressure_This_Week_TS
Text item=Indoor_Min_Pressure_This_Month_TS
Text item=Indoor_Max_Pressure_TS
Text item=Indoor_Max_Pressure_This_Week_TS
Text item=Indoor_Max_Pressure_This_Month_TS
Text item=Indoor_AbsolutePressure
Text item=Indoor_Noise
Text item=Indoor_Min_Noise
Text item=Indoor_Min_Noise_This_Week
Text item=Indoor_Min_Noise_This_Month
Text item=Indoor_Max_Noise
Text item=Indoor_Max_Noise_This_Week
Text item=Indoor_Max_Noise_This_Month
Text item=Indoor_Min_Noise_TS
Text item=Indoor_Min_Noise_This_Week_TS
Text item=Indoor_Min_Noise_This_Month_TS
Text item=Indoor_Max_Noise_TS
Text item=Indoor_Max_Noise_This_Week_TS
Text item=Indoor_Max_Noise_This_Month_TS
Text item=Indoor_WifiStatus
Text item=Indoor_TimeStamp
Text item=Indoor_Location
Text item=Indoor_LastStatusStore
}
Frame label="Outdoor" {
Text item=Outdoor_Temperature
Text item=Outdoor_TempTrend
Text item=Outdoor_Humidity
Text item=Outdoor_Humidex valuecolor=[<20.1="green",<29.1="blue",<28.1="yellow",<45.1="orange",<54.1="red",>54.1="maroon"]
Text item=Outdoor_HeatIndex
Text item=Outdoor_Dewpoint
Text item=Outdoor_DewpointDepression
Text item=Outdoor_RfStatus
Text item=Outdoor_LowBattery
Text item=Outdoor_BatteryVP
Text item=Outdoor_TimeStamp
Text item=Outdoor_LastMessage
}
Frame label="Rain" {
Text item=Rain_Hour
Text item=Rain_Today
Text item=Rain_Week
Text item=Rain_Month
Text item=Rain_BatteryVP
}
}
```
# Sample data
If you want to evaluate this binding but have not got a Netatmo station yourself
yet, you can add the Netatmo office in Paris to your account:
http://www.netatmo.com/en-US/addguest/index/TIQ3797dtfOmgpqUcct3/70:ee:50:00:02:20
# Icons
The following icons are used by original Netatmo web app:
## Modules
- https://my.netatmo.com/images/my/app/module_int.png
- https://my.netatmo.com/images/my/app/module_ext.png
- https://my.netatmo.com/images/my/app/module_rain.png
## Battery status
- https://my.netatmo.com/images/my/app/battery_verylow.png
- https://my.netatmo.com/images/my/app/battery_low.png
- https://my.netatmo.com/images/my/app/battery_medium.png
- https://my.netatmo.com/images/my/app/battery_high.png
- https://my.netatmo.com/images/my/app/battery_full.png
## Signal status
- https://my.netatmo.com/images/my/app/signal_verylow.png
- https://my.netatmo.com/images/my/app/signal_low.png
- https://my.netatmo.com/images/my/app/signal_medium.png
- https://my.netatmo.com/images/my/app/signal_high.png
- https://my.netatmo.com/images/my/app/signal_full.png
## Wifi status
- https://my.netatmo.com/images/my/app/wifi_low.png
- https://my.netatmo.com/images/my/app/wifi_medium.png
- https://my.netatmo.com/images/my/app/wifi_high.png
- https://my.netatmo.com/images/my/app/wifi_full.png
@@ -0,0 +1,85 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
<version>3.0.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.binding.netatmo</artifactId>
<name>openHAB Add-ons :: Bundles :: Netatmo Binding</name>
<properties>
<dep.noembedding>com.squareup.okhttp.okhttp,com.squareup.okio.okio-1.3.0,com.squareup.retrofit.retrofit,org.apache.oltu.oauth2.client,org.apache.oltu.oauth2.common</dep.noembedding>
</properties>
<dependencies>
<dependency>
<groupId>org.openhab.osgiify</groupId>
<artifactId>org.json.json</artifactId>
<version>20131018</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.osgiify</groupId>
<artifactId>com.squareup.okhttp.okhttp</artifactId>
<version>2.3.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.osgiify</groupId>
<artifactId>com.squareup.okio.okio-1.3.0</artifactId>
<version>1.3.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.openhab.osgiify</groupId>
<artifactId>com.squareup.retrofit.retrofit</artifactId>
<version>1.9.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.apache.oltu.oauth2</groupId>
<artifactId>org.apache.oltu.oauth2.client</artifactId>
<version>1.0.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.apache.oltu.oauth2</groupId>
<artifactId>org.apache.oltu.oauth2.common</artifactId>
<version>1.0.0</version>
<scope>compile</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>io.swagger</groupId>
<artifactId>swagger-codegen-maven-plugin</artifactId>
<version>2.1.6</version>
<executions>
<execution>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<inputSpec>https://raw.githubusercontent.com/cbornet/netatmo-swagger-decl/35e27745fb0d432bc6c8b5ec7a83ed2a09944cea/spec/swagger.yaml</inputSpec>
<language>java</language>
<library>retrofit</library>
<configOptions>
<sourceFolder>src/main/java</sourceFolder>
</configOptions>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
@@ -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>
@@ -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;
}
}
@@ -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;
}
}
@@ -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());
}
@@ -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();
}
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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);
}
}
@@ -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;
}
}
@@ -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();
}
}
@@ -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();
}
}
@@ -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;
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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);
}
}
}
@@ -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);
}
@@ -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);
}
}
@@ -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;
}
}
@@ -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());
}
}
@@ -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();
}
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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());
}
}
@@ -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());
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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);
}
}
@@ -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();
}
}
@@ -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();
}
}
@@ -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>
@@ -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>
@@ -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
@@ -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
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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;
}
}
}
@@ -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();
}
}
}
@@ -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;
}
}
}