]> git.basschouten.com Git - openhab-addons.git/commitdiff
[somneo] Initial contribution (#12321)
author0x4d4d <20473294+0x4d4d@users.noreply.github.com>
Wed, 27 Jul 2022 08:38:06 +0000 (10:38 +0200)
committerGitHub <noreply@github.com>
Wed, 27 Jul 2022 08:38:06 +0000 (10:38 +0200)
* [somneo] Initial contribution

Signed-off-by: Michael Myrcik <michael.myrcik@web.de>
29 files changed:
CODEOWNERS
bom/openhab-addons/pom.xml
bundles/org.openhab.binding.somneo/NOTICE [new file with mode: 0644]
bundles/org.openhab.binding.somneo/README.md [new file with mode: 0644]
bundles/org.openhab.binding.somneo/pom.xml [new file with mode: 0644]
bundles/org.openhab.binding.somneo/src/main/feature/feature.xml [new file with mode: 0644]
bundles/org.openhab.binding.somneo/src/main/java/org/openhab/binding/somneo/internal/HttpClientProvider.java [new file with mode: 0644]
bundles/org.openhab.binding.somneo/src/main/java/org/openhab/binding/somneo/internal/SomneoBindingConstants.java [new file with mode: 0644]
bundles/org.openhab.binding.somneo/src/main/java/org/openhab/binding/somneo/internal/SomneoConfiguration.java [new file with mode: 0644]
bundles/org.openhab.binding.somneo/src/main/java/org/openhab/binding/somneo/internal/SomneoHandler.java [new file with mode: 0644]
bundles/org.openhab.binding.somneo/src/main/java/org/openhab/binding/somneo/internal/SomneoHandlerFactory.java [new file with mode: 0644]
bundles/org.openhab.binding.somneo/src/main/java/org/openhab/binding/somneo/internal/SomneoHttpConnector.java [new file with mode: 0644]
bundles/org.openhab.binding.somneo/src/main/java/org/openhab/binding/somneo/internal/SomneoPresetStateDescriptionProvider.java [new file with mode: 0644]
bundles/org.openhab.binding.somneo/src/main/java/org/openhab/binding/somneo/internal/model/AudioData.java [new file with mode: 0644]
bundles/org.openhab.binding.somneo/src/main/java/org/openhab/binding/somneo/internal/model/DeviceData.java [new file with mode: 0644]
bundles/org.openhab.binding.somneo/src/main/java/org/openhab/binding/somneo/internal/model/FirmwareData.java [new file with mode: 0644]
bundles/org.openhab.binding.somneo/src/main/java/org/openhab/binding/somneo/internal/model/LightData.java [new file with mode: 0644]
bundles/org.openhab.binding.somneo/src/main/java/org/openhab/binding/somneo/internal/model/PresetData.java [new file with mode: 0644]
bundles/org.openhab.binding.somneo/src/main/java/org/openhab/binding/somneo/internal/model/RadioData.java [new file with mode: 0644]
bundles/org.openhab.binding.somneo/src/main/java/org/openhab/binding/somneo/internal/model/RelaxData.java [new file with mode: 0644]
bundles/org.openhab.binding.somneo/src/main/java/org/openhab/binding/somneo/internal/model/SensorData.java [new file with mode: 0644]
bundles/org.openhab.binding.somneo/src/main/java/org/openhab/binding/somneo/internal/model/SunsetData.java [new file with mode: 0644]
bundles/org.openhab.binding.somneo/src/main/java/org/openhab/binding/somneo/internal/model/TimerData.java [new file with mode: 0644]
bundles/org.openhab.binding.somneo/src/main/java/org/openhab/binding/somneo/internal/model/WifiData.java [new file with mode: 0644]
bundles/org.openhab.binding.somneo/src/main/resources/OH-INF/binding/binding.xml [new file with mode: 0644]
bundles/org.openhab.binding.somneo/src/main/resources/OH-INF/i18n/somneo.properties [new file with mode: 0644]
bundles/org.openhab.binding.somneo/src/main/resources/OH-INF/i18n/somneo_de.properties [new file with mode: 0644]
bundles/org.openhab.binding.somneo/src/main/resources/OH-INF/thing/thing-types.xml [new file with mode: 0644]
bundles/pom.xml

index 64ab31aa37a2d4de1902585521f7348d4cb9550d..7d4e10888c3c1ec3826181fe90b6e920e8f7e4d2 100644 (file)
 /bundles/org.openhab.binding.solarwatt/ @sven-carstens
 /bundles/org.openhab.binding.somfymylink/ @loungeflyz
 /bundles/org.openhab.binding.somfytahoma/ @octa22
+/bundles/org.openhab.binding.somneo/ @0x4d4d
 /bundles/org.openhab.binding.sonnen/ @chingon007
 /bundles/org.openhab.binding.sonos/ @kgoderis @lolodomo
 /bundles/org.openhab.binding.sonyaudio/ @freke
index 8cddb09a3adcc5c1c2ab2481302dd7e41ee5340b..688265456c2ea907afdcb1bfd74b45da277e349f 100644 (file)
       <artifactId>org.openhab.binding.somfytahoma</artifactId>
       <version>${project.version}</version>
     </dependency>
+    <dependency>
+      <groupId>org.openhab.addons.bundles</groupId>
+      <artifactId>org.openhab.binding.somneo</artifactId>
+      <version>${project.version}</version>
+    </dependency>
     <dependency>
       <groupId>org.openhab.addons.bundles</groupId>
       <artifactId>org.openhab.binding.sonnen</artifactId>
diff --git a/bundles/org.openhab.binding.somneo/NOTICE b/bundles/org.openhab.binding.somneo/NOTICE
new file mode 100644 (file)
index 0000000..38d625e
--- /dev/null
@@ -0,0 +1,13 @@
+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
diff --git a/bundles/org.openhab.binding.somneo/README.md b/bundles/org.openhab.binding.somneo/README.md
new file mode 100644 (file)
index 0000000..70cca0f
--- /dev/null
@@ -0,0 +1,152 @@
+# Philips Somneo Binding
+
+This binding integrates Philips Somneo HF367X into openHAB.
+
+## Supported Things
+
+This binding does only support one Thing:
+
+- `Philips Somneo HF367X`: A connected sleep and wake-Up light with the ThingTypeUID `hf367x`
+
+## Thing Configuration
+
+The Philips Somneo thing requires the `hostname` it can connect to.
+Its API only allows HTTPS access, but unfortunately the SSL certificate is not trusted and must be ignored by the parameter.
+
+| Parameter           | Values                                    | Default |
+|---------------------|-------------------------------------------|---------|
+| hostname            | Hostname or IP address of the device      | -       |
+| port                | Port number                               | 443     |
+| refreshInterval     | Interval the device is polled in sec      | 30      |
+| ignoreSSLErrors     | Ignore SSL Errors                         | true    |
+
+## Channels
+
+| Channel               | Type                 | Read/Write | Description                                                 |
+|-----------------------|----------------------|------------|-------------------------------------------------------------|
+| _Sensor_              |                      |            |                                                             |
+| sensor#illuminance    | Number:Illuminance   | R          | The current illuminance in lux                              |
+| sensor#temperature    | Number:Temperature   | R          | The current temperature                                     |
+| sensor#humidity       | Number:Dimensionless | R          | The current humidity in %                                   |
+| sensor#noise          | Number:Dimensionless | R          | The current noise in dB                                     |
+| _Light_               |                      |            |                                                             |
+| light#main            | Switch               | RW         | Turn the light on, off and set the brightness               |
+| light#night           | Switch               | RW         | Turn the night light on or off                              |
+| _Sunset_              |                      |            |                                                             |
+| sunset#switch         | Switch               | RW         | Turn the sunset program on or off                           |
+| sunset#remainingTime  | Number:Time          | R          | Remaining time from an activated program                    |
+| sunset#lightIntensity | Dimmer               | RW         | Set the brightness during the sunset programme              |
+| sunset#duration       | Number:Time          | RW         | The duration of sunset program in minutes                   |
+| sunset#colorSchema    | Number               | RW         | Choose a personal sunset                                    |
+| sunset#ambientNoise   | String               | RW         | Ambient noise played during the sunset                      |
+| sunset#volume         | Dimmer               | RW         | Set the volume during the sunset programme                  |
+| _Relax_               |                      |            |                                                             |
+| relax#switch          | Switch               | RW         | Turn the relax breathe program on or off                    |
+| relax#remainingTime   | Number:Time          | R          | Remaining time from an activated program                    |
+| relax#breathingRate   | Number               | RW         | Breathing rate per minute during the relax program          |
+| relax#duration        | Number:Time          | RW         | The duration of breathe program in minutes                  |
+| relax#guidanceType    | Number               | RW         | Select a breath guidance type during the relax program      |
+| relax#lightIntensity  | Dimmer               | RW         | Set the brightness during the breathe programme             |
+| relax#volume          | Dimmer               | RW         | Set the volume during the breathe programme                 |
+| _Audio_               |                      |            |                                                             |
+| audio#radio           | Player               | RW         | Controlling the radio and seeking for a frequency           |
+| audio#aux             | Switch               | RW         | Turn the AUX input on or off                                |
+| audio#volume          | Dimmer               | RW         | Change the sound volume of the device                       |
+| audio#preset          | String               | RW         | The Device has 5 presets to store radio frequencies         |
+| audio#frequency       | String               | R          | The currently selected radio frequency                      |
+
+## Full Example
+
+somneo.things:
+
+```
+Thing somneo:hf367x:1 "Philips Somneo" @ "Bedroom" [ hostname="192.168.0.110", ignoreSSLErrors=true ]
+```
+
+somneo.items:
+
+```
+// Sensors
+Number:Illuminance   PhilipsSomneo_Illuminance "Illuminance" <Sun>         ["Measurement", "Light"]       { channel="somneo:hf367x:1:sensor#illuminance" }
+Number:Temperature   PhilipsSomneo_Temperature "Temperature" <Temperature> ["Measurement", "Temperature"] { channel="somneo:hf367x:1:sensor#temperature" }
+Number:Dimensionless PhilipsSomneo_Humidity    "Humidity"    <Humidity>    ["Measurement", "Humidity"]    { channel="somneo:hf367x:1:sensor#humidity" }
+Number:Dimensionless PhilipsSomneo_Noise       "Noise"       <Noise>       ["Measurement", "Noise"]       { channel="somneo:hf367x:1:sensor#noise" }
+// Light
+Dimmer PhilipsSomneo_MainLight       "Light"        <Light> ["Control", "Light"] { channel="somneo:hf367x:1:light#main" }
+Switch PhilipsSomneo_NightLite       "Night Light"  <Light> ["Control", "Light"] { channel="somneo:hf367x:1:light#night" }
+// Sunset
+Switch      PhilipsSomneo_SunsetSwitch    "Sunset Program"  <Light>       ["Switch", "Power"]             { channel="somneo:hf367x:1:sunset#switch" }
+Number:Time PhilipsSomneo_SunsetRemaining "Remaining Time"  <Time>        ["Status", "Duration"]          { channel="somneo:hf367x:1:sunset#remainingTime" }
+Dimmer      PhilipsSomneo_SunsetIntensity "Light Intensity" <Light>       ["Control", "Light"]            { channel="somneo:hf367x:1:sunset#lightIntensity" }
+Number:Time PhilipsSomneo_SunsetDuration  "Duration"        <Time>        ["Control", "Duration"]         { channel="somneo:hf367x:1:sunset#duration" }
+Number      PhilipsSomneo_SunsetColor     "Sunset Color"    <Sunset>      ["Control", "ColorTemperature"] { channel="somneo:hf367x:1:sunset#colorSchema" }
+String      PhilipsSomneo_SunsetNoise     "Ambient Noise"   <Noise>       ["Control", "Noise"]            { channel="somneo:hf367x:1:sunset#ambientNoise" }
+Dimmer      PhilipsSomneo_SunsetVolume    "Volume"          <SoundVolume> ["Control", "SoundVolume"]      { channel="somneo:hf367x:1:sunset#volume" }
+// Relax
+Switch      PhilipsSomneo_RelaxSwitch        "Relax Program"   <Light>       ["Switch", "Power"]        { channel="somneo:hf367x:1:relax#switch" }
+Number:Time PhilipsSomneo_RelaxRemaining     "Remaining Time"  <Time>        ["Status", "Duration"]     { channel="somneo:hf367x:1:relax#remainingTime" }
+Number      PhilipsSomneo_RelaxBreathingRate "Breathing Rate"                ["Control"]                { channel="somneo:hf367x:1:relax#breathingRate" }
+Number:Time PhilipsSomneo_RelaxDuration      "Duration"        <Time>        ["Control", "Duration"]    { channel="somneo:hf367x:1:relax#duration" }
+Number      PhilipsSomneo_RelaxGuidanceType  "Guidance Type"                 ["Control"]                { channel="somneo:hf367x:1:relax#guidanceType" }
+Dimmer      PhilipsSomneo_RelaxIntensity     "Light Intensity" <Light>       ["Control", "Light"]       { channel="somneo:hf367x:1:relax#lightIntensity" }
+Dimmer      PhilipsSomneo_RelaxVolume        "Volume"          <SoundVolume> ["Control", "SoundVolume"] { channel="somneo:hf367x:1:relax#volume" }
+// Audio
+Player PhilipsSomneo_AudioRadio     "Radio Control" <MediaControl> ["Control"]                { channel="somneo:hf367x:1:audio#radio" }
+Switch PhilipsSomneo_AudioAux       "AUX-Input"                    ["Switch", "Power"]        { channel="somneo:hf367x:1:audio#aux" }
+Dimmer PhilipsSomneo_AudioVolume    "Volume"        <SoundVolume>  ["Control", "SoundVolume"] { channel="somneo:hf367x:1:audio#volume" }
+String PhilipsSomneo_AudioPreset    "FM Preset"                    ["Control"]                { channel="somneo:hf367x:1:audio#preset" }
+String PhilipsSomneo_AudioFrequency "FM Frequency"                 ["Status"]                 { channel="somneo:hf367x:1:audio#frequency" }
+```
+
+somneo.sitemap:
+
+```
+sitemap somneo label="Philips Somneo" {
+    Frame label="Sensors" {
+        Default item=PhilipsSomneo_Illuminance
+        Default item=PhilipsSomneo_Temperature
+        Default item=PhilipsSomneo_Humidity
+        Default item=PhilipsSomneo_Noise
+    }
+    Frame label="Lights" {
+        Default item=PhilipsSomneo_MainLight
+        Default item=PhilipsSomneo_MainLight visibility=[PhilipsSomneo_MainLight>0]
+        Default item=PhilipsSomneo_NightLite
+    }
+    Frame label="Programs" {
+        Default item=PhilipsSomneo_SunsetSwitch
+        Default item=PhilipsSomneo_SunsetRemaining visibility=[PhilipsSomneo_SunsetSwitch==ON]
+        Text label="Sunset Settings" icon="settings" {
+            Default item=PhilipsSomneo_SunsetIntensity
+            Default item=PhilipsSomneo_SunsetDuration
+            Selection item=PhilipsSomneo_SunsetColor
+            Default item=PhilipsSomneo_SunsetNoise
+            Default item=PhilipsSomneo_SunsetVolume
+        }
+        Default item=PhilipsSomneo_RelaxSwitch
+        Default item=PhilipsSomneo_RelaxRemaining visibility=[PhilipsSomneo_RelaxSwitch==ON]
+        Text label="Relax Settings" icon="settings" {
+            Default item=PhilipsSomneo_RelaxBreathingRate
+            Selection item=PhilipsSomneo_RelaxDuration
+            Switch item=PhilipsSomneo_RelaxGuidanceType mappings=[0="Light", 1="Sound"]
+            Default item=PhilipsSomneo_RelaxIntensity
+            Default item=PhilipsSomneo_RelaxVolume
+        }
+    }
+    Frame label="Audio" {
+        Default item=PhilipsSomneo_AudioRadio
+        Default item=PhilipsSomneo_AudioAux
+        Default item=PhilipsSomneo_AudioVolume visibility=[PhilipsSomneo_AudioRadio==PLAY, PhilipsSomneo_AudioAux==ON]
+        Default item=PhilipsSomneo_AudioPreset visibility=[PhilipsSomneo_AudioRadio==PLAY]
+        Default item=PhilipsSomneo_AudioFrequency visibility=[PhilipsSomneo_AudioRadio==PLAY]
+    }
+}
+```
+
+## Acknowledgements
+
+Thanks to:
+
+* [homebridge-somneo](https://github.com/zackwag/homebridge-somneo) - For creating a similar plugin in another platform and exposing endpoints for control.
+* [somneo-client](https://github.com/DonkerNet/somneo-client) - For creating a similar plugin in another platform and exposing endpoints for control.
+* HTTP Binding and other OpenHAB addons - Which was used as examples.
diff --git a/bundles/org.openhab.binding.somneo/pom.xml b/bundles/org.openhab.binding.somneo/pom.xml
new file mode 100644 (file)
index 0000000..aedb97c
--- /dev/null
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://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.4.0-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>org.openhab.binding.somneo</artifactId>
+
+  <name>openHAB Add-ons :: Bundles :: Somneo Binding</name>
+
+</project>
diff --git a/bundles/org.openhab.binding.somneo/src/main/feature/feature.xml b/bundles/org.openhab.binding.somneo/src/main/feature/feature.xml
new file mode 100644 (file)
index 0000000..108ecb0
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<features name="org.openhab.binding.somneo-${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-somneo" description="Somneo Binding" version="${project.version}">
+               <feature>openhab-runtime-base</feature>
+               <bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.somneo/${project.version}</bundle>
+       </feature>
+</features>
diff --git a/bundles/org.openhab.binding.somneo/src/main/java/org/openhab/binding/somneo/internal/HttpClientProvider.java b/bundles/org.openhab.binding.somneo/src/main/java/org/openhab/binding/somneo/internal/HttpClientProvider.java
new file mode 100644 (file)
index 0000000..c839c6f
--- /dev/null
@@ -0,0 +1,40 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.somneo.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jetty.client.HttpClient;
+
+/**
+ * The {@link HttpClientProvider} defines the interface for providing
+ * {@link HttpClient} instances to thing handlers.
+ *
+ * @author Michael Myrcik - Initial contribution
+ */
+@NonNullByDefault
+public interface HttpClientProvider {
+
+    /**
+     * Get the secure http client
+     *
+     * @return a HttpClient
+     */
+    HttpClient getSecureClient();
+
+    /**
+     * Get the insecure http client (ignores SSL errors)
+     *
+     * @return a HttpClient
+     */
+    HttpClient getInsecureClient();
+}
diff --git a/bundles/org.openhab.binding.somneo/src/main/java/org/openhab/binding/somneo/internal/SomneoBindingConstants.java b/bundles/org.openhab.binding.somneo/src/main/java/org/openhab/binding/somneo/internal/SomneoBindingConstants.java
new file mode 100644 (file)
index 0000000..780b497
--- /dev/null
@@ -0,0 +1,74 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.somneo.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.thing.ThingTypeUID;
+
+/**
+ * The {@link SomneoBindingConstants} class defines common constants, which are
+ * used across the whole binding.
+ *
+ * @author Michael Myrcik - Initial contribution
+ */
+@NonNullByDefault
+public class SomneoBindingConstants {
+
+    private static final String BINDING_ID = "somneo";
+
+    // List of all Thing properties
+    public static final String PROPERTY_VENDOR_NAME = "Philips";
+
+    // List of all Thing Type UIDs
+    public static final ThingTypeUID THING_TYPE_HF367X = new ThingTypeUID(BINDING_ID, "hf367x");
+
+    // List of all Channel ids
+    public static final String CHANNEL_AUDIO_AUX = "audio#aux";
+    public static final String CHANNEL_AUDIO_FREQUENCY = "audio#frequency";
+    public static final String CHANNEL_AUDIO_PRESET = "audio#preset";
+    public static final String CHANNEL_AUDIO_RADIO = "audio#radio";
+    public static final String CHANNEL_AUDIO_VOLUME = "audio#volume";
+    public static final String CHANNEL_LIGHT_MAIN = "light#main";
+    public static final String CHANNEL_LIGHT_NIGHT = "light#night";
+    public static final String CHANNEL_RELAX_BREATHING_RATE = "relax#breathingRate";
+    public static final String CHANNEL_RELAX_DURATION = "relax#duration";
+    public static final String CHANNEL_RELAX_GUIDANCE_TYPE = "relax#guidanceType";
+    public static final String CHANNEL_RELAX_LIGHT_INTENSITY = "relax#lightIntensity";
+    public static final String CHANNEL_RELAX_REMAINING_TIME = "relax#remainingTime";
+    public static final String CHANNEL_RELAX_SWITCH = "relax#switch";
+    public static final String CHANNEL_RELAX_VOLUME = "relax#volume";
+    public static final String CHANNEL_SENSOR_ILLUMINANCE = "sensor#illuminance";
+    public static final String CHANNEL_SENSOR_HUMIDITY = "sensor#humidity";
+    public static final String CHANNEL_SENSOR_NOISE = "sensor#noise";
+    public static final String CHANNEL_SENSOR_TEMPERATURE = "sensor#temperature";
+    public static final String CHANNEL_SUNSET_AMBIENT_NOISE = "sunset#ambientNoise";
+    public static final String CHANNEL_SUNSET_COLOR_SCHEMA = "sunset#colorSchema";
+    public static final String CHANNEL_SUNSET_DURATION = "sunset#duration";
+    public static final String CHANNEL_SUNSET_LIGHT_INTENSITY = "sunset#lightIntensity";
+    public static final String CHANNEL_SUNSET_REMAINING_TIME = "sunset#remainingTime";
+    public static final String CHANNEL_SUNSET_SWITCH = "sunset#switch";
+    public static final String CHANNEL_SUNSET_VOLUME = "sunset#volume";
+
+    // List of all Web Service Endpoints
+    public static final String AUDIO_ENDPOINT = "/1/wuply";
+    public static final String DEVICE_ENDPOINT = "/1/device";
+    public static final String FIRMWARE_ENDPOINT = "/0/firmware";
+    public static final String LIGHT_ENDPOINT = "/1/wulgt";
+    public static final String PRESET_ENDPOINT = "/1/wufmp/00";
+    public static final String RADIO_ENDPOINT = "/1/wufmr";
+    public static final String RELAX_ENDPOINT = "/1/wurlx";
+    public static final String TIMER_ENDPOINT = "/1/wutmr";
+    public static final String SENSORS_ENDPOINT = "/1/wusrd";
+    public static final String SUNSET_ENDPOINT = "/1/wudsk";
+    public static final String WIFI_ENDPOINT = "/0/wifi";
+}
diff --git a/bundles/org.openhab.binding.somneo/src/main/java/org/openhab/binding/somneo/internal/SomneoConfiguration.java b/bundles/org.openhab.binding.somneo/src/main/java/org/openhab/binding/somneo/internal/SomneoConfiguration.java
new file mode 100644 (file)
index 0000000..c173e16
--- /dev/null
@@ -0,0 +1,29 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.somneo.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link SomneoConfiguration} class contains fields mapping thing configuration parameters.
+ *
+ * @author Michael Myrcik - Initial contribution
+ */
+@NonNullByDefault
+public class SomneoConfiguration {
+
+    public String hostname = "";
+    public int port = 443;
+    public int refreshInterval = 30;
+    public boolean ignoreSSLErrors = false;
+}
diff --git a/bundles/org.openhab.binding.somneo/src/main/java/org/openhab/binding/somneo/internal/SomneoHandler.java b/bundles/org.openhab.binding.somneo/src/main/java/org/openhab/binding/somneo/internal/SomneoHandler.java
new file mode 100644 (file)
index 0000000..39c74f8
--- /dev/null
@@ -0,0 +1,544 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.somneo.internal;
+
+import static org.openhab.binding.somneo.internal.SomneoBindingConstants.*;
+
+import java.io.EOFException;
+import java.util.Map;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
+import org.openhab.binding.somneo.internal.model.AudioData;
+import org.openhab.binding.somneo.internal.model.DeviceData;
+import org.openhab.binding.somneo.internal.model.FirmwareData;
+import org.openhab.binding.somneo.internal.model.LightData;
+import org.openhab.binding.somneo.internal.model.PresetData;
+import org.openhab.binding.somneo.internal.model.RadioData;
+import org.openhab.binding.somneo.internal.model.RelaxData;
+import org.openhab.binding.somneo.internal.model.SensorData;
+import org.openhab.binding.somneo.internal.model.SunsetData;
+import org.openhab.binding.somneo.internal.model.TimerData;
+import org.openhab.binding.somneo.internal.model.WifiData;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.NextPreviousType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.PercentType;
+import org.openhab.core.library.types.PlayPauseType;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.library.unit.Units;
+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.BaseThingHandler;
+import org.openhab.core.types.Command;
+import org.openhab.core.types.RefreshType;
+import org.openhab.core.types.State;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link SomneoHandler} is responsible for handling commands, which are
+ * sent to one of the channels.
+ *
+ * @author Michael Myrcik - Initial contribution
+ */
+@NonNullByDefault
+public class SomneoHandler extends BaseThingHandler {
+
+    private final Logger logger = LoggerFactory.getLogger(SomneoHandler.class);
+
+    private final HttpClientProvider httpClientProvider;
+
+    private final SomneoPresetStateDescriptionProvider provider;
+
+    /**
+     * Job to poll data from the device.
+     */
+    private @Nullable ScheduledFuture<?> pollingJob;
+
+    /**
+     * Job to count down the remaining program time.
+     */
+    private @Nullable ScheduledFuture<?> remainingTimerJob;
+
+    private @Nullable SomneoHttpConnector connector;
+
+    /**
+     * Cache the last brightness level in order to know the correct level when the
+     * ON command is given.
+     */
+    private volatile int lastLightBrightness;
+
+    private volatile int remainingTimeRelax;
+
+    private volatile int remainingTimeSunset;
+
+    public SomneoHandler(Thing thing, HttpClientProvider httpClientProvider,
+            SomneoPresetStateDescriptionProvider provider) {
+        super(thing);
+        this.httpClientProvider = httpClientProvider;
+        this.provider = provider;
+    }
+
+    @Override
+    public void handleCommand(ChannelUID channelUID, Command command) {
+        String channelId = channelUID.getId();
+        logger.debug("Handle command '{}' for channel {}", command, channelId);
+
+        if (command instanceof RefreshType) {
+            this.poll();
+            return;
+        }
+
+        final SomneoHttpConnector connector = this.connector;
+        if (connector == null) {
+            return;
+        }
+
+        try {
+            switch (channelId) {
+                case CHANNEL_AUDIO_AUX:
+                    if (command instanceof OnOffType) {
+                        boolean isOn = OnOffType.ON.equals(command);
+                        connector.switchAux(isOn);
+
+                        if (isOn) {
+                            updateState(CHANNEL_AUDIO_RADIO, PlayPauseType.PAUSE);
+                            updateState(CHANNEL_RELAX_SWITCH, OnOffType.OFF);
+                            updateState(CHANNEL_SUNSET_SWITCH, OnOffType.OFF);
+                        }
+                    }
+                    break;
+                case CHANNEL_AUDIO_PRESET:
+                    if (command instanceof StringType) {
+                        connector.setRadioChannel(command.toFullString());
+
+                        updateState(CHANNEL_AUDIO_RADIO, PlayPauseType.PLAY);
+                        updateState(CHANNEL_AUDIO_AUX, OnOffType.OFF);
+                        updateState(CHANNEL_RELAX_SWITCH, OnOffType.OFF);
+                        updateState(CHANNEL_SUNSET_SWITCH, OnOffType.OFF);
+
+                        updateFrequency();
+                    }
+                    break;
+                case CHANNEL_AUDIO_RADIO:
+                    if (command instanceof PlayPauseType) {
+                        boolean isPlaying = PlayPauseType.PLAY.equals(command);
+                        connector.switchRadio(isPlaying);
+
+                        if (isPlaying) {
+                            updateState(CHANNEL_AUDIO_AUX, OnOffType.OFF);
+                            updateState(CHANNEL_RELAX_SWITCH, OnOffType.OFF);
+                            updateState(CHANNEL_SUNSET_SWITCH, OnOffType.OFF);
+                        }
+                    } else if (command instanceof NextPreviousType && NextPreviousType.NEXT.equals(command)) {
+                        connector.radioSeekUp();
+
+                        updateFrequency();
+                    } else if (command instanceof NextPreviousType && NextPreviousType.PREVIOUS.equals(command)) {
+                        connector.radioSeekDown();
+
+                        updateFrequency();
+                    }
+                    break;
+                case CHANNEL_AUDIO_VOLUME:
+                    if (command instanceof PercentType) {
+                        connector.setAudioVolume(Integer.parseInt(command.toFullString()));
+                    }
+                    break;
+                case CHANNEL_LIGHT_MAIN:
+                    if (command instanceof OnOffType) {
+                        boolean isOn = OnOffType.ON.equals(command);
+                        connector.switchMainLight(isOn);
+
+                        if (isOn) {
+                            updateState(CHANNEL_LIGHT_MAIN, new PercentType(lastLightBrightness));
+                            updateState(CHANNEL_LIGHT_NIGHT, OnOffType.OFF);
+                            updateState(CHANNEL_RELAX_SWITCH, OnOffType.OFF);
+                            updateState(CHANNEL_SUNSET_SWITCH, OnOffType.OFF);
+                        }
+                    }
+                    if (command instanceof PercentType) {
+                        int level = Integer.parseInt(command.toFullString());
+
+                        if (level > 0) {
+                            connector.setMainLightDimmer(level);
+                            lastLightBrightness = level;
+
+                            updateState(CHANNEL_LIGHT_NIGHT, OnOffType.OFF);
+                            updateState(CHANNEL_RELAX_SWITCH, OnOffType.OFF);
+                            updateState(CHANNEL_SUNSET_SWITCH, OnOffType.OFF);
+                        } else {
+                            connector.switchMainLight(false);
+                        }
+                    }
+                    break;
+                case CHANNEL_LIGHT_NIGHT:
+                    if (command instanceof OnOffType) {
+                        boolean isOn = OnOffType.ON.equals(command);
+                        connector.switchNightLight(isOn);
+
+                        if (isOn) {
+                            updateState(CHANNEL_LIGHT_MAIN, OnOffType.OFF);
+                            updateState(CHANNEL_RELAX_SWITCH, OnOffType.OFF);
+                            updateState(CHANNEL_SUNSET_SWITCH, OnOffType.OFF);
+                        }
+                    }
+                    break;
+                case CHANNEL_RELAX_BREATHING_RATE:
+                    if (command instanceof DecimalType) {
+                        connector.setRelaxBreathingRate(Integer.parseInt(command.toFullString()));
+                    }
+                    break;
+                case CHANNEL_RELAX_DURATION:
+                    if (command instanceof DecimalType) {
+                        connector.setRelaxDuration(Integer.parseInt(command.toFullString()));
+                    }
+                    break;
+                case CHANNEL_RELAX_GUIDANCE_TYPE:
+                    if (command instanceof DecimalType) {
+                        connector.setRelaxGuidanceType(Integer.parseInt(command.toFullString()));
+                    }
+                    break;
+                case CHANNEL_RELAX_LIGHT_INTENSITY:
+                    if (command instanceof PercentType) {
+                        connector.setRelaxLightIntensity(Integer.parseInt(command.toFullString()));
+                    }
+                    break;
+                case CHANNEL_RELAX_SWITCH:
+                    if (command instanceof OnOffType) {
+                        boolean isOn = OnOffType.ON.equals(command);
+                        connector.switchRelaxProgram(isOn);
+
+                        updateRemainingTimer();
+
+                        if (isOn) {
+                            updateState(CHANNEL_AUDIO_AUX, OnOffType.OFF);
+                            updateState(CHANNEL_AUDIO_RADIO, PlayPauseType.PAUSE);
+                            updateState(CHANNEL_LIGHT_MAIN, OnOffType.OFF);
+                            updateState(CHANNEL_LIGHT_NIGHT, OnOffType.OFF);
+                            updateState(CHANNEL_SUNSET_SWITCH, OnOffType.OFF);
+                        }
+                    }
+                    break;
+                case CHANNEL_RELAX_VOLUME:
+                    if (command instanceof PercentType) {
+                        connector.setRelaxVolume(Integer.parseInt(command.toFullString()));
+                    }
+                    break;
+                case CHANNEL_SUNSET_AMBIENT_NOISE:
+                    if (command instanceof StringType) {
+                        connector.setSunsetAmbientNoise(command.toFullString());
+                    }
+                    break;
+                case CHANNEL_SUNSET_COLOR_SCHEMA:
+                    if (command instanceof DecimalType) {
+                        connector.setSunsetColorSchema(Integer.parseInt(command.toFullString()));
+                    }
+                    break;
+                case CHANNEL_SUNSET_DURATION:
+                    if (command instanceof DecimalType) {
+                        connector.setSunsetDuration(Integer.parseInt(command.toFullString()));
+                    }
+                    break;
+                case CHANNEL_SUNSET_LIGHT_INTENSITY:
+                    if (command instanceof PercentType) {
+                        connector.setSunsetLightIntensity(Integer.parseInt(command.toFullString()));
+                    }
+                    break;
+                case CHANNEL_SUNSET_SWITCH:
+                    if (command instanceof OnOffType) {
+                        boolean isOn = OnOffType.ON.equals(command);
+                        connector.switchSunsetProgram(isOn);
+
+                        updateRemainingTimer();
+
+                        if (isOn) {
+                            updateState(CHANNEL_AUDIO_AUX, OnOffType.OFF);
+                            updateState(CHANNEL_AUDIO_RADIO, PlayPauseType.PAUSE);
+                            updateState(CHANNEL_LIGHT_MAIN, OnOffType.OFF);
+                            updateState(CHANNEL_LIGHT_NIGHT, OnOffType.OFF);
+                            updateState(CHANNEL_RELAX_SWITCH, OnOffType.OFF);
+                        }
+                    }
+                    break;
+                case CHANNEL_SUNSET_VOLUME:
+                    if (command instanceof PercentType) {
+                        connector.setSunsetVolume(Integer.parseInt(command.toFullString()));
+                    }
+                    break;
+                default:
+                    logger.warn("Received unknown channel {}", channelId);
+                    break;
+            }
+        } catch (InterruptedException e) {
+            logger.debug("Handle command interrupted");
+            Thread.currentThread().interrupt();
+        } catch (TimeoutException | ExecutionException e) {
+            if (e.getCause() instanceof EOFException) {
+                // Occurs on parallel mobile app access
+                logger.debug("EOF: {}", e.getMessage());
+            } else {
+                updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
+            }
+        }
+    }
+
+    @Override
+    public void initialize() {
+        updateStatus(ThingStatus.UNKNOWN);
+
+        initConnector();
+        updateThingProperties();
+        startPolling();
+    }
+
+    @Override
+    public void dispose() {
+        stopPolling();
+        stopRemainingTimer();
+
+        super.dispose();
+    }
+
+    private void initConnector() {
+        if (connector == null) {
+            SomneoConfiguration config = getConfigAs(SomneoConfiguration.class);
+            HttpClient httpClient;
+            if (config.ignoreSSLErrors) {
+                logger.info("Using the insecure client for thing '{}'.", thing.getUID());
+                httpClient = httpClientProvider.getInsecureClient();
+            } else {
+                logger.info("Using the secure client for thing '{}'.", thing.getUID());
+                httpClient = httpClientProvider.getSecureClient();
+            }
+
+            connector = new SomneoHttpConnector(config, httpClient);
+        }
+    }
+
+    private void updateThingProperties() {
+        final SomneoHttpConnector connector = this.connector;
+        if (connector == null) {
+            return;
+        }
+
+        Map<String, String> properties = editProperties();
+        properties.put(Thing.PROPERTY_VENDOR, PROPERTY_VENDOR_NAME);
+
+        try {
+            final DeviceData deviceData = connector.fetchDeviceData();
+            String value = deviceData.getModelId();
+            if (value != null) {
+                properties.put(Thing.PROPERTY_MODEL_ID, value);
+            }
+            value = deviceData.getSerial();
+            if (value != null) {
+                properties.put(Thing.PROPERTY_SERIAL_NUMBER, value);
+            }
+
+            final WifiData wifiData = connector.fetchWifiData();
+            value = wifiData.getMacAddress();
+            if (value != null) {
+                properties.put(Thing.PROPERTY_MAC_ADDRESS, value);
+            }
+
+            final FirmwareData firmwareData = connector.fetchFirmwareData();
+            value = firmwareData.getVersion();
+            if (value != null) {
+                properties.put(Thing.PROPERTY_FIRMWARE_VERSION, value);
+            }
+
+            updateProperties(properties);
+        } catch (InterruptedException e) {
+            logger.debug("Update properties interrupted");
+            Thread.currentThread().interrupt();
+        } catch (TimeoutException | ExecutionException e) {
+            if (e.getCause() instanceof EOFException) {
+                // Occurs on parallel mobile app access
+                logger.debug("EOF: {}", e.getMessage());
+            } else {
+                updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
+            }
+        }
+    }
+
+    /**
+     * Set up the connection to the receiver by starting to poll the HTTP API.
+     */
+    private void startPolling() {
+        final ScheduledFuture<?> pollingJob = this.pollingJob;
+        if (pollingJob != null && !pollingJob.isCancelled()) {
+            return;
+        }
+
+        int refreshInterval = getConfigAs(SomneoConfiguration.class).refreshInterval;
+        logger.debug("Start polling job at interval {}s", refreshInterval);
+        this.pollingJob = scheduler.scheduleWithFixedDelay(this::poll, 0, refreshInterval, TimeUnit.SECONDS);
+    }
+
+    private void stopPolling() {
+        final ScheduledFuture<?> pollingJob = this.pollingJob;
+        if (pollingJob == null || pollingJob.isCancelled()) {
+            return;
+        }
+
+        pollingJob.cancel(true);
+        this.pollingJob = null;
+        logger.debug("HTTP polling stopped.");
+    }
+
+    private void poll() {
+        final SomneoHttpConnector connector = this.connector;
+        if (connector == null) {
+            return;
+        }
+
+        try {
+            final SensorData sensorData = connector.fetchSensorData();
+            updateState(CHANNEL_SENSOR_HUMIDITY, sensorData.getCurrentHumidity());
+            updateState(CHANNEL_SENSOR_ILLUMINANCE, sensorData.getCurrentIlluminance());
+            updateState(CHANNEL_SENSOR_NOISE, sensorData.getCurrentNoise());
+            updateState(CHANNEL_SENSOR_TEMPERATURE, sensorData.getCurrentTemperature());
+
+            final LightData lightData = connector.fetchLightData();
+            updateState(CHANNEL_LIGHT_MAIN, lightData.getMainLightState());
+            updateState(CHANNEL_LIGHT_NIGHT, lightData.getNightLightState());
+            lastLightBrightness = lightData.getMainLightLevel();
+
+            final SunsetData sunsetData = connector.fetchSunsetData();
+            updateState(CHANNEL_SUNSET_SWITCH, sunsetData.getSwitchState());
+            updateState(CHANNEL_SUNSET_LIGHT_INTENSITY, sunsetData.getLightIntensity());
+            updateState(CHANNEL_SUNSET_DURATION, sunsetData.getDurationInMin());
+            updateState(CHANNEL_SUNSET_COLOR_SCHEMA, sunsetData.getColorSchema());
+            updateState(CHANNEL_SUNSET_AMBIENT_NOISE, sunsetData.getAmbientNoise());
+            updateState(CHANNEL_SUNSET_VOLUME, sunsetData.getSoundVolume());
+
+            final RelaxData relaxData = connector.fetchRelaxData();
+            updateState(CHANNEL_RELAX_SWITCH, relaxData.getSwitchState());
+            updateState(CHANNEL_RELAX_BREATHING_RATE, relaxData.getBreathingRate());
+            updateState(CHANNEL_RELAX_DURATION, relaxData.getDurationInMin());
+            updateState(CHANNEL_RELAX_GUIDANCE_TYPE, relaxData.getGuidanceType());
+            updateState(CHANNEL_RELAX_LIGHT_INTENSITY, relaxData.getLightIntensity());
+            updateState(CHANNEL_RELAX_VOLUME, relaxData.getSoundVolume());
+
+            final AudioData audioData = connector.fetchAudioData();
+            updateState(CHANNEL_AUDIO_RADIO, audioData.getRadioState());
+            updateState(CHANNEL_AUDIO_AUX, audioData.getAuxState());
+            updateState(CHANNEL_AUDIO_VOLUME, audioData.getVolumeState());
+            updateState(CHANNEL_AUDIO_PRESET, audioData.getPresetState());
+
+            updateFrequency();
+
+            updateRemainingTimer();
+
+            updateStatus(ThingStatus.ONLINE);
+        } catch (InterruptedException e) {
+            logger.debug("Polling data interrupted");
+            Thread.currentThread().interrupt();
+        } catch (TimeoutException | ExecutionException e) {
+            if (e.getCause() instanceof EOFException) {
+                // Occurs on parallel mobile app access
+                logger.debug("EOF: {}", e.getMessage());
+            } else {
+                updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
+            }
+        }
+    }
+
+    private void updateFrequency() throws TimeoutException, InterruptedException, ExecutionException {
+        final SomneoHttpConnector connector = this.connector;
+        if (connector == null) {
+            return;
+        }
+
+        RadioData radioData = connector.getRadioData();
+        updateState(CHANNEL_AUDIO_FREQUENCY, radioData.getFrequency());
+
+        final PresetData presetData = connector.fetchPresetData();
+        final Channel presetChannel = getThing().getChannel(CHANNEL_AUDIO_PRESET);
+        if (presetChannel != null) {
+            provider.setStateOptions(presetChannel.getUID(), presetData.createPresetOptions());
+        }
+    }
+
+    private void updateRemainingTimer() throws TimeoutException, InterruptedException, ExecutionException {
+        final SomneoHttpConnector connector = this.connector;
+        if (connector == null) {
+            return;
+        }
+
+        TimerData timerData = connector.fetchTimerData();
+
+        remainingTimeRelax = timerData.remainingTimeRelax();
+        remainingTimeSunset = timerData.remainingTimeSunset();
+
+        if (remainingTimeRelax > 0 || remainingTimeSunset > 0) {
+            startRemainingTimer();
+        } else {
+            State state = new QuantityType<>(0, Units.SECOND);
+            updateState(CHANNEL_RELAX_REMAINING_TIME, state);
+            updateState(CHANNEL_SUNSET_REMAINING_TIME, state);
+        }
+    }
+
+    private void startRemainingTimer() {
+        final ScheduledFuture<?> remainingTimerJob = this.remainingTimerJob;
+        if (remainingTimerJob != null && !remainingTimerJob.isCancelled()) {
+            return;
+        }
+
+        logger.debug("Start remaining timer ticker job");
+        this.remainingTimerJob = scheduler.scheduleWithFixedDelay(this::remainingTimerTick, 0, 1, TimeUnit.SECONDS);
+    }
+
+    private void stopRemainingTimer() {
+        final ScheduledFuture<?> remainingTimerJob = this.remainingTimerJob;
+        if (remainingTimerJob == null || remainingTimerJob.isCancelled()) {
+            return;
+        }
+
+        remainingTimerJob.cancel(true);
+        this.remainingTimerJob = null;
+        logger.debug("Remaining timer ticker stopped.");
+    }
+
+    private void remainingTimerTick() {
+        if (remainingTimeRelax > 0) {
+            remainingTimeRelax--;
+
+            State state = new QuantityType<>(remainingTimeRelax, Units.SECOND);
+            updateState(CHANNEL_RELAX_REMAINING_TIME, state);
+        }
+
+        if (remainingTimeSunset > 0) {
+            remainingTimeSunset--;
+
+            State state = new QuantityType<>(remainingTimeSunset, Units.SECOND);
+            updateState(CHANNEL_SUNSET_REMAINING_TIME, state);
+        }
+
+        if (remainingTimeRelax <= 0 && remainingTimeSunset <= 0) {
+            stopRemainingTimer();
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.somneo/src/main/java/org/openhab/binding/somneo/internal/SomneoHandlerFactory.java b/bundles/org.openhab.binding.somneo/src/main/java/org/openhab/binding/somneo/internal/SomneoHandlerFactory.java
new file mode 100644 (file)
index 0000000..66a9acd
--- /dev/null
@@ -0,0 +1,93 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.somneo.internal;
+
+import static org.openhab.binding.somneo.internal.SomneoBindingConstants.THING_TYPE_HF367X;
+
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.ThingTypeUID;
+import org.openhab.core.thing.binding.BaseThingHandlerFactory;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerFactory;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link SomneoHandlerFactory} is responsible for creating things and thing
+ * handlers.
+ *
+ * @author Michael Myrcik - Initial contribution
+ */
+@NonNullByDefault
+@Component(configurationPid = "binding.somneo", service = ThingHandlerFactory.class)
+public class SomneoHandlerFactory extends BaseThingHandlerFactory implements HttpClientProvider {
+
+    private final Logger logger = LoggerFactory.getLogger(SomneoHandlerFactory.class);
+
+    private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_HF367X);
+
+    private final HttpClient secureClient;
+    private final HttpClient insecureClient;
+    private final SomneoPresetStateDescriptionProvider provider;
+
+    @Activate
+    public SomneoHandlerFactory(@Reference SomneoPresetStateDescriptionProvider provider) {
+        this.provider = provider;
+
+        this.secureClient = new HttpClient(new SslContextFactory.Client(false));
+        this.insecureClient = new HttpClient(new SslContextFactory.Client(true));
+
+        try {
+            this.secureClient.start();
+            this.insecureClient.start();
+        } catch (Exception e) {
+            logger.warn("Failed to start insecure http client: {}", e.getMessage());
+            throw new IllegalStateException("Could not create insecure HttpClient");
+        }
+    }
+
+    @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 (THING_TYPE_HF367X.equals(thingTypeUID)) {
+            return new SomneoHandler(thing, this, provider);
+        }
+
+        return null;
+    }
+
+    @Override
+    public HttpClient getSecureClient() {
+        return secureClient;
+    }
+
+    @Override
+    public HttpClient getInsecureClient() {
+        return insecureClient;
+    }
+}
diff --git a/bundles/org.openhab.binding.somneo/src/main/java/org/openhab/binding/somneo/internal/SomneoHttpConnector.java b/bundles/org.openhab.binding.somneo/src/main/java/org/openhab/binding/somneo/internal/SomneoHttpConnector.java
new file mode 100644 (file)
index 0000000..6bfa1d2
--- /dev/null
@@ -0,0 +1,356 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.somneo.internal;
+
+import static org.openhab.binding.somneo.internal.SomneoBindingConstants.*;
+
+import java.io.UnsupportedEncodingException;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.client.util.StringContentProvider;
+import org.eclipse.jetty.http.HttpMethod;
+import org.eclipse.jetty.http.HttpStatus;
+import org.openhab.binding.somneo.internal.model.AudioData;
+import org.openhab.binding.somneo.internal.model.DeviceData;
+import org.openhab.binding.somneo.internal.model.FirmwareData;
+import org.openhab.binding.somneo.internal.model.LightData;
+import org.openhab.binding.somneo.internal.model.PresetData;
+import org.openhab.binding.somneo.internal.model.RadioData;
+import org.openhab.binding.somneo.internal.model.RelaxData;
+import org.openhab.binding.somneo.internal.model.SensorData;
+import org.openhab.binding.somneo.internal.model.SunsetData;
+import org.openhab.binding.somneo.internal.model.TimerData;
+import org.openhab.binding.somneo.internal.model.WifiData;
+import org.openhab.core.io.net.http.HttpUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.Gson;
+
+/**
+ * The {@link SomneoHttpConnector} is responsible for sending commands.
+ *
+ * @author Michael Myrcik - Initial contribution
+ */
+@NonNullByDefault
+public class SomneoHttpConnector {
+
+    private Logger logger = LoggerFactory.getLogger(SomneoHttpConnector.class);
+
+    private static final int REQUEST_TIMEOUT_MS = 5000;
+
+    private static final String DEFAULT_CONTENT_TYPE = "application/json";
+
+    private final Gson gson = new Gson();
+
+    private final HttpClient httpClient;
+
+    private final String urlBase;
+
+    public SomneoHttpConnector(SomneoConfiguration config, HttpClient httpClient) {
+        this.httpClient = httpClient;
+        this.urlBase = String.format("https://%s:%d/di/v1/products", config.hostname, config.port);
+    }
+
+    public SensorData fetchSensorData() throws TimeoutException, InterruptedException, ExecutionException {
+        return executeUrl("GET", SENSORS_ENDPOINT, SensorData.class);
+    }
+
+    public LightData fetchLightData() throws TimeoutException, InterruptedException, ExecutionException {
+        return executeUrl("GET", LIGHT_ENDPOINT, LightData.class);
+    }
+
+    public SunsetData fetchSunsetData() throws TimeoutException, InterruptedException, ExecutionException {
+        return executeUrl("GET", SUNSET_ENDPOINT, SunsetData.class);
+    }
+
+    public void switchMainLight(boolean state) throws TimeoutException, InterruptedException, ExecutionException {
+        final LightData data = new LightData();
+        data.setMainLight(state);
+        data.setNightLight(false);
+        data.setPreviewLight(false);
+
+        executeUrl("PUT", LIGHT_ENDPOINT, data);
+    }
+
+    public void setMainLightDimmer(int level) throws TimeoutException, InterruptedException, ExecutionException {
+        final LightData data = new LightData();
+        data.setMainLightLevel(level);
+        data.setMainLight(true);
+        data.setNightLight(false);
+        data.setPreviewLight(false);
+
+        executeUrl("PUT", LIGHT_ENDPOINT, data);
+    }
+
+    public void switchNightLight(boolean state) throws TimeoutException, InterruptedException, ExecutionException {
+        final LightData data = new LightData();
+        data.setMainLight(false);
+        data.setNightLight(state);
+        data.setPreviewLight(false);
+
+        executeUrl("PUT", LIGHT_ENDPOINT, data);
+    }
+
+    public void switchSunsetProgram(boolean state) throws TimeoutException, InterruptedException, ExecutionException {
+        final SunsetData data = new SunsetData();
+        data.setState(state);
+
+        executeUrl("PUT", SUNSET_ENDPOINT, data);
+    }
+
+    public void setSunsetLightIntensity(int percent) throws TimeoutException, InterruptedException, ExecutionException {
+        final SunsetData data = new SunsetData();
+        data.setLightIntensity(percent);
+
+        executeUrl("PUT", SUNSET_ENDPOINT, data);
+    }
+
+    public void setSunsetDuration(int duration) throws TimeoutException, InterruptedException, ExecutionException {
+        final SunsetData data = new SunsetData();
+        data.setDurationInMin(duration);
+
+        executeUrl("PUT", SUNSET_ENDPOINT, data);
+    }
+
+    public void setSunsetColorSchema(int value) throws TimeoutException, InterruptedException, ExecutionException {
+        final SunsetData data = new SunsetData();
+        data.setColorSchema(value);
+
+        executeUrl("PUT", SUNSET_ENDPOINT, data);
+    }
+
+    public void setSunsetAmbientNoise(String option) throws TimeoutException, InterruptedException, ExecutionException {
+        final SunsetData data = new SunsetData();
+        data.setAmbientNoise(option);
+
+        executeUrl("PUT", SUNSET_ENDPOINT, data);
+    }
+
+    public void setSunsetVolume(int percent) throws TimeoutException, InterruptedException, ExecutionException {
+        final SunsetData data = new SunsetData();
+        data.setSoundVolume(percent);
+
+        executeUrl("PUT", SUNSET_ENDPOINT, data);
+    }
+
+    public RelaxData fetchRelaxData() throws TimeoutException, InterruptedException, ExecutionException {
+        return executeUrl("GET", RELAX_ENDPOINT, RelaxData.class);
+    }
+
+    public void setRelaxVolume(int percent) throws TimeoutException, InterruptedException, ExecutionException {
+        final RelaxData data = new RelaxData();
+        data.setSoundVolume(percent);
+
+        executeUrl("PUT", RELAX_ENDPOINT, data);
+    }
+
+    public void setRelaxLightIntensity(int percent) throws TimeoutException, InterruptedException, ExecutionException {
+        final RelaxData data = new RelaxData();
+        data.setLightIntensity(percent);
+
+        executeUrl("PUT", RELAX_ENDPOINT, data);
+    }
+
+    public void switchRelaxProgram(boolean state) throws TimeoutException, InterruptedException, ExecutionException {
+        final RelaxData data = new RelaxData();
+        data.setState(state);
+
+        executeUrl("PUT", RELAX_ENDPOINT, data);
+    }
+
+    public void setRelaxBreathingRate(int value) throws TimeoutException, InterruptedException, ExecutionException {
+        final RelaxData data = new RelaxData();
+        data.setBreathingRate(value);
+
+        executeUrl("PUT", RELAX_ENDPOINT, data);
+    }
+
+    public void setRelaxDuration(int value) throws TimeoutException, InterruptedException, ExecutionException {
+        final RelaxData data = new RelaxData();
+        data.setDurationInMin(value);
+
+        executeUrl("PUT", RELAX_ENDPOINT, data);
+    }
+
+    public void setRelaxGuidanceType(int value) throws TimeoutException, InterruptedException, ExecutionException {
+        final RelaxData data = new RelaxData();
+        data.setGuidanceType(value);
+
+        executeUrl("PUT", RELAX_ENDPOINT, data);
+    }
+
+    public AudioData fetchAudioData() throws TimeoutException, InterruptedException, ExecutionException {
+        return executeUrl("GET", AUDIO_ENDPOINT, AudioData.class);
+    }
+
+    public void switchRadio(boolean state) throws TimeoutException, InterruptedException, ExecutionException {
+        final AudioData data = new AudioData();
+        if (state) {
+            data.enableRadio();
+        } else {
+            data.disableAudio();
+        }
+
+        executeUrl("PUT", AUDIO_ENDPOINT, data);
+    }
+
+    public void switchAux(boolean state) throws TimeoutException, InterruptedException, ExecutionException {
+        final AudioData data = new AudioData();
+        if (state) {
+            data.enableAux();
+        } else {
+            data.disableAudio();
+        }
+
+        executeUrl("PUT", AUDIO_ENDPOINT, data);
+    }
+
+    public void setAudioVolume(int percent) throws TimeoutException, InterruptedException, ExecutionException {
+        final AudioData data = new AudioData();
+        data.setVolume(percent);
+
+        executeUrl("PUT", AUDIO_ENDPOINT, data);
+    }
+
+    public void setRadioChannel(String preset) throws TimeoutException, InterruptedException, ExecutionException {
+        final AudioData data = new AudioData();
+        data.enableRadio();
+        data.setRadioPreset(preset);
+
+        executeUrl("PUT", AUDIO_ENDPOINT, data);
+    }
+
+    public RadioData getRadioData() throws TimeoutException, InterruptedException, ExecutionException {
+        RadioData data = new RadioData();
+        int loops = 0;
+        do {
+            if (loops > 20) {
+                break;
+            }
+            if (loops > 0) {
+                loops++;
+                Thread.sleep(250);
+            }
+            data = executeUrl("GET", RADIO_ENDPOINT, RadioData.class);
+        } while (data.isSeeking()); // Wait until seek is finished
+
+        return data;
+    }
+
+    public void radioSeekUp() throws TimeoutException, InterruptedException, ExecutionException {
+        final RadioData data = new RadioData();
+        data.setCmdSeekUp();
+
+        executeUrl("PUT", RADIO_ENDPOINT, data);
+    }
+
+    public void radioSeekDown() throws TimeoutException, InterruptedException, ExecutionException {
+        final RadioData data = new RadioData();
+        data.setCmdSeekDown();
+
+        executeUrl("PUT", RADIO_ENDPOINT, data);
+    }
+
+    public DeviceData fetchDeviceData() throws TimeoutException, InterruptedException, ExecutionException {
+        return executeUrl("GET", DEVICE_ENDPOINT, DeviceData.class);
+    }
+
+    public WifiData fetchWifiData() throws TimeoutException, InterruptedException, ExecutionException {
+        return executeUrl("GET", WIFI_ENDPOINT, WifiData.class);
+    }
+
+    public FirmwareData fetchFirmwareData() throws TimeoutException, InterruptedException, ExecutionException {
+        return executeUrl("GET", FIRMWARE_ENDPOINT, FirmwareData.class);
+    }
+
+    public TimerData fetchTimerData() throws TimeoutException, InterruptedException, ExecutionException {
+        return executeUrl("GET", TIMER_ENDPOINT, TimerData.class);
+    }
+
+    public PresetData fetchPresetData() throws TimeoutException, InterruptedException, ExecutionException {
+        return executeUrl("GET", PRESET_ENDPOINT, PresetData.class);
+    }
+
+    private <T> T executeUrl(String httpMethod, String endpoint, Class<T> classOfT)
+            throws TimeoutException, InterruptedException, ExecutionException {
+        final String responseBody = executeUrl("GET", endpoint, (String) null);
+        final T data = gson.fromJson(responseBody, classOfT);
+        return data;
+    }
+
+    private void executeUrl(String httpMethod, String endpoint, Object data)
+            throws TimeoutException, InterruptedException, ExecutionException {
+        final String content = gson.toJson(data);
+        executeUrl(httpMethod, endpoint, content);
+    }
+
+    /**
+     * Executes the given <code>url</code> with the given <code>httpMethod</code>
+     *
+     * @param httpMethod the HTTP method to use
+     * @param endpoint the url endpoint
+     * @param content the content to be sent to the given <code>url</code> or
+     *            <code>null</code> if no content should be sent.
+     * @return
+     * @throws ExecutionException
+     * @throws InterruptedException
+     * @throws UnsupportedEncodingException
+     * @throws Exception when the request execution failed, timed out or it was interrupted
+     */
+    private String executeUrl(String httpMethod, String endpoint, @Nullable String content)
+            throws TimeoutException, InterruptedException, ExecutionException {
+        final String url = urlBase + endpoint;
+        final HttpMethod method = HttpUtil.createHttpMethod(httpMethod);
+
+        final Request request = httpClient.newRequest(url).method(method).timeout(REQUEST_TIMEOUT_MS,
+                TimeUnit.MILLISECONDS);
+
+        if (content != null && (HttpMethod.POST.equals(method) || HttpMethod.PUT.equals(method))) {
+            final StringContentProvider stringContentProvider = new StringContentProvider(content,
+                    StandardCharsets.UTF_8);
+            request.content(stringContentProvider, DEFAULT_CONTENT_TYPE);
+
+            logger.trace("Request for url '{}':\r\n{}", url, content);
+        } else {
+            logger.trace("Request for url '{}'", url);
+        }
+
+        final ContentResponse response = request.send();
+        final int statusCode = response.getStatus();
+        if (logger.isDebugEnabled() && statusCode >= HttpStatus.BAD_REQUEST_400) {
+            String statusLine = statusCode + " " + response.getReason();
+            logger.debug("Method failed: {}", statusLine);
+        }
+
+        final String encoding = response.getEncoding() != null ? response.getEncoding().replaceAll("\"", "").trim()
+                : StandardCharsets.UTF_8.name();
+
+        try {
+            String responseBody = new String(response.getContent(), encoding);
+            logger.trace("Response for url '{}':\r\n{}", url, responseBody);
+            return responseBody;
+        } catch (UnsupportedEncodingException e) {
+            logger.warn("Get response content failed!", e);
+            return "";
+        }
+    }
+}
diff --git a/bundles/org.openhab.binding.somneo/src/main/java/org/openhab/binding/somneo/internal/SomneoPresetStateDescriptionProvider.java b/bundles/org.openhab.binding.somneo/src/main/java/org/openhab/binding/somneo/internal/SomneoPresetStateDescriptionProvider.java
new file mode 100644 (file)
index 0000000..42378f7
--- /dev/null
@@ -0,0 +1,42 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.somneo.internal;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.events.EventPublisher;
+import org.openhab.core.thing.binding.BaseDynamicStateDescriptionProvider;
+import org.openhab.core.thing.i18n.ChannelTypeI18nLocalizationService;
+import org.openhab.core.thing.link.ItemChannelLinkRegistry;
+import org.openhab.core.thing.type.DynamicStateDescriptionProvider;
+import org.osgi.service.component.annotations.Activate;
+import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Reference;
+
+/**
+ * Dynamic channel state description provider.
+ * 
+ * @author Michael Myrcik - Initial contribution
+ */
+@Component(service = { DynamicStateDescriptionProvider.class, SomneoPresetStateDescriptionProvider.class })
+@NonNullByDefault
+public class SomneoPresetStateDescriptionProvider extends BaseDynamicStateDescriptionProvider {
+
+    @Activate
+    public SomneoPresetStateDescriptionProvider(final @Reference EventPublisher eventPublisher,
+            final @Reference ItemChannelLinkRegistry itemChannelLinkRegistry,
+            final @Reference ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) {
+        this.eventPublisher = eventPublisher;
+        this.itemChannelLinkRegistry = itemChannelLinkRegistry;
+        this.channelTypeI18nLocalizationService = channelTypeI18nLocalizationService;
+    }
+}
diff --git a/bundles/org.openhab.binding.somneo/src/main/java/org/openhab/binding/somneo/internal/model/AudioData.java b/bundles/org.openhab.binding.somneo/src/main/java/org/openhab/binding/somneo/internal/model/AudioData.java
new file mode 100644 (file)
index 0000000..25fc821
--- /dev/null
@@ -0,0 +1,125 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.somneo.internal.model;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.PercentType;
+import org.openhab.core.library.types.PlayPauseType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * This class represents the audio state from the API.
+ *
+ * @author Michael Myrcik - Initial contribution
+ */
+@NonNullByDefault
+public class AudioData {
+
+    private static final String SOURCE_RADIO = "fmr";
+
+    private static final String SOURCE_AUX = "aux";
+
+    private static final String SOURCE_OFF = "off";
+
+    @SerializedName("onoff")
+    private @Nullable Boolean power;
+
+    /**
+     * Must be set to false when the audio is turned on, otherwise a light that is
+     * turned on will be turned off.
+     */
+    @SuppressWarnings("unused")
+    @SerializedName("tempy")
+    private @Nullable Boolean previewLight;
+
+    /**
+     * Volume range from 0 to 25.
+     */
+    @SerializedName("sdvol")
+    private @Nullable Integer volume;
+
+    /**
+     * Current active audio source. Can be radio, aux or off.
+     */
+    @SerializedName("snddv")
+    private @Nullable String source;
+
+    /**
+     * Current active radio preset.
+     */
+    @SerializedName("sndch")
+    private @Nullable String preset;
+
+    public void disableAudio() {
+        power = false;
+        source = SOURCE_OFF;
+    }
+
+    public void enableRadio() {
+        power = true;
+        source = SOURCE_RADIO;
+        previewLight = false;
+    }
+
+    public State getRadioState() {
+        final Boolean power = this.power;
+        if (power == null) {
+            return UnDefType.NULL;
+        }
+        return power && SOURCE_RADIO.equals(source) ? PlayPauseType.PLAY : PlayPauseType.PAUSE;
+    }
+
+    public void enableAux() {
+        power = true;
+        source = SOURCE_AUX;
+        previewLight = false;
+    }
+
+    public State getAuxState() {
+        final Boolean power = this.power;
+        if (power == null) {
+            return UnDefType.NULL;
+        }
+        return OnOffType.from(power && SOURCE_AUX.equals(source));
+    }
+
+    public void setVolume(int percent) {
+        this.volume = percent / 4;
+    }
+
+    public State getVolumeState() {
+        final Integer volume = this.volume;
+        if (volume == null) {
+            return UnDefType.NULL;
+        }
+        return new PercentType(volume * 4);
+    }
+
+    public void setRadioPreset(String preset) {
+        this.preset = preset;
+    }
+
+    public State getPresetState() {
+        final String preset = this.preset;
+        if (preset == null) {
+            return UnDefType.NULL;
+        }
+        return new StringType(preset);
+    }
+}
diff --git a/bundles/org.openhab.binding.somneo/src/main/java/org/openhab/binding/somneo/internal/model/DeviceData.java b/bundles/org.openhab.binding.somneo/src/main/java/org/openhab/binding/somneo/internal/model/DeviceData.java
new file mode 100644 (file)
index 0000000..af25fce
--- /dev/null
@@ -0,0 +1,41 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.somneo.internal.model;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * This class represents the device data from the API.
+ *
+ * @author Michael Myrcik - Initial contribution
+ */
+@NonNullByDefault
+public class DeviceData {
+
+    @SerializedName("type")
+    private @Nullable String modelId;
+
+    @SerializedName("serial")
+    private @Nullable String serial;
+
+    public @Nullable String getModelId() {
+        return modelId;
+    }
+
+    public @Nullable String getSerial() {
+        return serial;
+    }
+}
diff --git a/bundles/org.openhab.binding.somneo/src/main/java/org/openhab/binding/somneo/internal/model/FirmwareData.java b/bundles/org.openhab.binding.somneo/src/main/java/org/openhab/binding/somneo/internal/model/FirmwareData.java
new file mode 100644 (file)
index 0000000..20172da
--- /dev/null
@@ -0,0 +1,34 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.somneo.internal.model;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * This class represents the firmware data from the API.
+ *
+ * @author Michael Myrcik - Initial contribution
+ */
+@NonNullByDefault
+public class FirmwareData {
+
+    @SerializedName("version")
+    private @Nullable String version;
+
+    public @Nullable String getVersion() {
+        return version;
+    }
+}
diff --git a/bundles/org.openhab.binding.somneo/src/main/java/org/openhab/binding/somneo/internal/model/LightData.java b/bundles/org.openhab.binding.somneo/src/main/java/org/openhab/binding/somneo/internal/model/LightData.java
new file mode 100644 (file)
index 0000000..a5adaac
--- /dev/null
@@ -0,0 +1,94 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.somneo.internal.model;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.PercentType;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * This class represents the light state from the API.
+ *
+ * @author Michael Myrcik - Initial contribution
+ */
+@NonNullByDefault
+public class LightData {
+
+    /**
+     * Brightness range from 0 to 25.
+     */
+    @SerializedName("ltlvl")
+    private @Nullable Integer mainLightLevel;
+
+    @SerializedName("onoff")
+    private @Nullable Boolean mainLight;
+
+    @SuppressWarnings("unused")
+    @SerializedName("tempy")
+    private @Nullable Boolean previewLight;
+
+    @SerializedName("ngtlt")
+    private @Nullable Boolean nightLight;
+
+    public int getMainLightLevel() {
+        final Integer mainLightLevel = this.mainLightLevel;
+        if (mainLightLevel == null) {
+            return 0;
+        }
+        return mainLightLevel * 4;
+    }
+
+    public void setMainLightLevel(int mainLightLevel) {
+        this.mainLightLevel = mainLightLevel / 4;
+    }
+
+    public State getMainLightState() {
+        final Boolean mainLight = this.mainLight;
+        final Integer mainLightLevel = this.mainLightLevel;
+        if (mainLight == null) {
+            return UnDefType.NULL;
+        }
+        if (mainLightLevel == null) {
+            return UnDefType.NULL;
+        }
+        if (mainLight) {
+            return new PercentType(mainLightLevel * 4);
+        }
+        return OnOffType.OFF;
+    }
+
+    public void setMainLight(boolean mainLight) {
+        this.mainLight = mainLight;
+    }
+
+    public void setPreviewLight(boolean previewLight) {
+        this.previewLight = previewLight;
+    }
+
+    public State getNightLightState() {
+        final Boolean nightLight = this.nightLight;
+        if (nightLight == null) {
+            return UnDefType.NULL;
+        }
+        return OnOffType.from(nightLight);
+    }
+
+    public void setNightLight(@Nullable Boolean nightLight) {
+        this.nightLight = nightLight;
+    }
+}
diff --git a/bundles/org.openhab.binding.somneo/src/main/java/org/openhab/binding/somneo/internal/model/PresetData.java b/bundles/org.openhab.binding.somneo/src/main/java/org/openhab/binding/somneo/internal/model/PresetData.java
new file mode 100644 (file)
index 0000000..381c10f
--- /dev/null
@@ -0,0 +1,69 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.somneo.internal.model;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.types.StateOption;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * This class represents the preset state from the API.
+ *
+ * @author Michael Myrcik - Initial contribution
+ */
+@NonNullByDefault
+public class PresetData {
+
+    private static final String LABEL_TEMPLATE = "%s fm";
+    private static final String LABEL_EMPTY_TEMPLATE = "FM %s";
+
+    @SerializedName("1")
+    private @Nullable String preset1;
+
+    @SerializedName("2")
+    private @Nullable String preset2;
+
+    @SerializedName("3")
+    private @Nullable String preset3;
+
+    @SerializedName("4")
+    private @Nullable String preset4;
+
+    @SerializedName("5")
+    private @Nullable String preset5;
+
+    public List<StateOption> createPresetOptions() {
+        List<StateOption> stateOptions = new ArrayList<>();
+        stateOptions.add(createStateOption("1", preset1));
+        stateOptions.add(createStateOption("2", preset2));
+        stateOptions.add(createStateOption("3", preset3));
+        stateOptions.add(createStateOption("4", preset4));
+        stateOptions.add(createStateOption("5", preset5));
+        return stateOptions;
+    }
+
+    private static StateOption createStateOption(String index, @Nullable String preset) {
+        String label;
+        if (preset == null || "".equals(preset)) {
+            label = String.format(LABEL_EMPTY_TEMPLATE, index);
+        } else {
+            label = String.format(LABEL_TEMPLATE, preset);
+        }
+        return new StateOption(index, label);
+    }
+}
diff --git a/bundles/org.openhab.binding.somneo/src/main/java/org/openhab/binding/somneo/internal/model/RadioData.java b/bundles/org.openhab.binding.somneo/src/main/java/org/openhab/binding/somneo/internal/model/RadioData.java
new file mode 100644 (file)
index 0000000..a5068f1
--- /dev/null
@@ -0,0 +1,62 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.somneo.internal.model;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * This class represents the radio state from the API.
+ *
+ * @author Michael Myrcik - Initial contribution
+ */
+@NonNullByDefault
+public class RadioData {
+
+    private static final String LABEL_TEMPLATE = "%s fm";
+
+    private static final String CMD_SEEK_UP = "seekup";
+
+    private static final String CMD_SEEK_DOWN = "seekdown";
+
+    @SerializedName("fmfrq")
+    private @Nullable String frequency;
+
+    @SerializedName("fmcmd")
+    private @Nullable String command;
+
+    public State getFrequency() {
+        final String frequency = this.frequency;
+        if (frequency == null) {
+            return UnDefType.NULL;
+        }
+        return new StringType(String.format(LABEL_TEMPLATE, frequency));
+    }
+
+    public void setCmdSeekUp() {
+        this.command = CMD_SEEK_UP;
+    }
+
+    public void setCmdSeekDown() {
+        this.command = CMD_SEEK_DOWN;
+    }
+
+    public boolean isSeeking() {
+        return CMD_SEEK_UP.equals(command) || CMD_SEEK_DOWN.equals(command);
+    }
+}
diff --git a/bundles/org.openhab.binding.somneo/src/main/java/org/openhab/binding/somneo/internal/model/RelaxData.java b/bundles/org.openhab.binding.somneo/src/main/java/org/openhab/binding/somneo/internal/model/RelaxData.java
new file mode 100644 (file)
index 0000000..7ffb77b
--- /dev/null
@@ -0,0 +1,128 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.somneo.internal.model;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.PercentType;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * This class represents the relax program state from the API.
+ *
+ * @author Michael Myrcik - Initial contribution
+ */
+@NonNullByDefault
+public class RelaxData {
+
+    @SerializedName("onoff")
+    private @Nullable Boolean state;
+
+    @SerializedName("progr")
+    private @Nullable Integer breathingRate;
+
+    @SerializedName("durat")
+    private @Nullable Integer durationInMin;
+
+    @SerializedName("rtype")
+    private @Nullable Integer guidanceType;
+
+    /**
+     * Brightness range from 0 to 25.
+     */
+    @SerializedName("intny")
+    private @Nullable Integer lightIntensity;
+
+    /**
+     * Volume range from 0 to 25.
+     */
+    @SerializedName("sndlv")
+    private @Nullable Integer soundVolume;
+
+    public State getSwitchState() {
+        final Boolean state = this.state;
+        if (state == null) {
+            return UnDefType.NULL;
+        }
+        return OnOffType.from(state);
+    }
+
+    public void setState(boolean state) {
+        this.state = state;
+    }
+
+    public State getBreathingRate() {
+        final Integer breathingRate = this.breathingRate;
+        if (breathingRate == null) {
+            return UnDefType.NULL;
+        }
+        return new DecimalType(breathingRate);
+    }
+
+    public void setBreathingRate(int breathingRate) {
+        this.breathingRate = breathingRate;
+    }
+
+    public State getDurationInMin() {
+        final Integer durationInMin = this.durationInMin;
+        if (durationInMin == null) {
+            return UnDefType.NULL;
+        }
+        return new DecimalType(durationInMin);
+    }
+
+    public void setDurationInMin(int durationInMin) {
+        this.durationInMin = durationInMin;
+    }
+
+    public State getGuidanceType() {
+        final Integer guidanceType = this.guidanceType;
+        if (guidanceType == null) {
+            return UnDefType.NULL;
+        }
+        return new DecimalType(guidanceType);
+    }
+
+    public void setGuidanceType(int guidanceType) {
+        this.guidanceType = guidanceType;
+    }
+
+    public State getLightIntensity() {
+        final Integer lightIntensity = this.lightIntensity;
+        if (lightIntensity == null) {
+            return UnDefType.NULL;
+        }
+        return new PercentType(lightIntensity * 4);
+    }
+
+    public void setLightIntensity(int percent) {
+        this.lightIntensity = percent / 4;
+    }
+
+    public State getSoundVolume() {
+        final Integer soundVolume = this.soundVolume;
+        if (soundVolume == null) {
+            return UnDefType.NULL;
+        }
+        return new PercentType(soundVolume * 4);
+    }
+
+    public void setSoundVolume(int percent) {
+        this.soundVolume = percent / 4;
+    }
+}
diff --git a/bundles/org.openhab.binding.somneo/src/main/java/org/openhab/binding/somneo/internal/model/SensorData.java b/bundles/org.openhab.binding.somneo/src/main/java/org/openhab/binding/somneo/internal/model/SensorData.java
new file mode 100644 (file)
index 0000000..dcdc6e5
--- /dev/null
@@ -0,0 +1,58 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.somneo.internal.model;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.core.library.types.QuantityType;
+import org.openhab.core.library.unit.SIUnits;
+import org.openhab.core.library.unit.Units;
+import org.openhab.core.types.State;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * This class represents the sensor state from the API.
+ *
+ * @author Michael Myrcik - Initial contribution
+ */
+@NonNullByDefault
+public class SensorData {
+
+    @SerializedName("mslux")
+    private float currentIlluminance;
+
+    @SerializedName("mstmp")
+    private float currentTemperature;
+
+    @SerializedName("msrhu")
+    private float currentHumidity;
+
+    @SerializedName("mssnd")
+    private int currentNoise;
+
+    public State getCurrentIlluminance() {
+        return new QuantityType<>(currentIlluminance, Units.LUX);
+    }
+
+    public State getCurrentTemperature() {
+        return new QuantityType<>(currentTemperature, SIUnits.CELSIUS);
+    }
+
+    public State getCurrentHumidity() {
+        return new QuantityType<>(currentHumidity, Units.PERCENT);
+    }
+
+    public State getCurrentNoise() {
+        return new QuantityType<>(currentNoise, Units.DECIBEL);
+    }
+}
diff --git a/bundles/org.openhab.binding.somneo/src/main/java/org/openhab/binding/somneo/internal/model/SunsetData.java b/bundles/org.openhab.binding.somneo/src/main/java/org/openhab/binding/somneo/internal/model/SunsetData.java
new file mode 100644 (file)
index 0000000..ae44dc5
--- /dev/null
@@ -0,0 +1,135 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.somneo.internal.model;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.OnOffType;
+import org.openhab.core.library.types.PercentType;
+import org.openhab.core.library.types.StringType;
+import org.openhab.core.types.State;
+import org.openhab.core.types.UnDefType;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * This class represents the sunset program state from the API.
+ *
+ * @author Michael Myrcik - Initial contribution
+ */
+@NonNullByDefault
+public class SunsetData {
+
+    @SerializedName("onoff")
+    private @Nullable Boolean state;
+
+    /**
+     * Brightness range from 0 to 25.
+     */
+    @SerializedName("curve")
+    private @Nullable Integer lightIntensity;
+
+    @SerializedName("durat")
+    private @Nullable Integer durationInMin;
+
+    @SerializedName("ctype")
+    private @Nullable Integer colorSchema;
+
+    @SerializedName("snddv")
+    private @Nullable String soundSource;
+
+    @SerializedName("sndch")
+    private @Nullable String ambientNoise;
+
+    /**
+     * Volume range from 0 to 25.
+     */
+    @SerializedName("sndlv")
+    private @Nullable Integer soundVolume;
+
+    public State getSwitchState() {
+        final Boolean state = this.state;
+        if (state == null) {
+            return UnDefType.NULL;
+        }
+        return OnOffType.from(state);
+    }
+
+    public void setState(boolean state) {
+        this.state = state;
+    }
+
+    public State getLightIntensity() {
+        final Integer lightIntensity = this.lightIntensity;
+        if (lightIntensity == null) {
+            return UnDefType.NULL;
+        }
+        return new PercentType(lightIntensity * 4);
+    }
+
+    public void setLightIntensity(int percent) {
+        this.lightIntensity = percent / 4;
+    }
+
+    public State getDurationInMin() {
+        final Integer durationInMin = this.durationInMin;
+        if (durationInMin == null) {
+            return UnDefType.NULL;
+        }
+        return new DecimalType(durationInMin);
+    }
+
+    public void setDurationInMin(int durationInMin) {
+        this.durationInMin = durationInMin;
+    }
+
+    public State getColorSchema() {
+        final Integer colorSchema = this.colorSchema;
+        if (colorSchema == null) {
+            return UnDefType.NULL;
+        }
+        return new DecimalType(colorSchema);
+    }
+
+    public void setColorSchema(int colorSchema) {
+        this.colorSchema = colorSchema;
+    }
+
+    public State getAmbientNoise() {
+        final String soundSource = this.soundSource;
+        if (soundSource == null) {
+            return UnDefType.NULL;
+        }
+        final String suffix = "off".equals(soundSource) ? "" : "-" + ambientNoise;
+        return new StringType(soundSource + suffix);
+    }
+
+    public void setAmbientNoise(String option) {
+        final String[] values = option.split("-");
+        soundSource = values[0];
+        ambientNoise = values.length == 1 ? "" : values[1];
+    }
+
+    public State getSoundVolume() {
+        final Integer soundVolume = this.soundVolume;
+        if (soundVolume == null) {
+            return UnDefType.NULL;
+        }
+        return new PercentType(soundVolume * 4);
+    }
+
+    public void setSoundVolume(int percent) {
+        this.soundVolume = percent / 4;
+    }
+}
diff --git a/bundles/org.openhab.binding.somneo/src/main/java/org/openhab/binding/somneo/internal/model/TimerData.java b/bundles/org.openhab.binding.somneo/src/main/java/org/openhab/binding/somneo/internal/model/TimerData.java
new file mode 100644 (file)
index 0000000..76cae07
--- /dev/null
@@ -0,0 +1,46 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.somneo.internal.model;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * This class represents the program timer state from the API.
+ *
+ * @author Michael Myrcik - Initial contribution
+ */
+@NonNullByDefault
+public class TimerData {
+
+    @SerializedName("rlxmn")
+    private int relaxMinutes;
+
+    @SerializedName("rlxsc")
+    private int relaxSeconds;
+
+    @SerializedName("dskmn")
+    private int sunsetMinutes;
+
+    @SerializedName("dsksc")
+    private int sunsetSeconds;
+
+    public int remainingTimeRelax() {
+        return relaxMinutes * 60 + relaxSeconds;
+    }
+
+    public int remainingTimeSunset() {
+        return sunsetMinutes * 60 + sunsetSeconds;
+    }
+}
diff --git a/bundles/org.openhab.binding.somneo/src/main/java/org/openhab/binding/somneo/internal/model/WifiData.java b/bundles/org.openhab.binding.somneo/src/main/java/org/openhab/binding/somneo/internal/model/WifiData.java
new file mode 100644 (file)
index 0000000..8862033
--- /dev/null
@@ -0,0 +1,34 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.somneo.internal.model;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+import com.google.gson.annotations.SerializedName;
+
+/**
+ * This class represents the wifi data from the API.
+ *
+ * @author Michael Myrcik - Initial contribution
+ */
+@NonNullByDefault
+public class WifiData {
+
+    @SerializedName("macaddress")
+    private @Nullable String macAddress;
+
+    public @Nullable String getMacAddress() {
+        return macAddress;
+    }
+}
diff --git a/bundles/org.openhab.binding.somneo/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.somneo/src/main/resources/OH-INF/binding/binding.xml
new file mode 100644 (file)
index 0000000..2034284
--- /dev/null
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<binding:binding id="somneo" 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>Somneo Binding</name>
+       <description>This is the binding for Philips Somneo devices.</description>
+
+</binding:binding>
diff --git a/bundles/org.openhab.binding.somneo/src/main/resources/OH-INF/i18n/somneo.properties b/bundles/org.openhab.binding.somneo/src/main/resources/OH-INF/i18n/somneo.properties
new file mode 100644 (file)
index 0000000..d5aa544
--- /dev/null
@@ -0,0 +1,108 @@
+# binding
+binding.somneo.name = Somneo Binding
+binding.somneo.description = This is the binding for Philips Somneo devices.
+
+# thing types
+thing-type.somneo.hf367x.label = Philips Somneo Light
+thing-type.somneo.hf367x.description = A smart sleep and wake-up light with sensors.
+
+# thing type config description
+thing-type.config.somneo.hf367x.hostname.label = Hostname
+thing-type.config.somneo.hf367x.hostname.description = Hostname or IP address of the device.
+thing-type.config.somneo.hf367x.ignoreSSLErrors.label = Ignore SSL Errors
+thing-type.config.somneo.hf367x.ignoreSSLErrors.description = If set to true ignores invalid SSL certificate errors. This is potentially dangerous.
+thing-type.config.somneo.hf367x.port.label = HTTP Port
+thing-type.config.somneo.hf367x.port.description = HTTP Port used for communication. Normally shouldn't be changed.
+thing-type.config.somneo.hf367x.refreshInterval.label = Refresh Interval
+thing-type.config.somneo.hf367x.refreshInterval.description = Interval the device is polled in sec.
+
+# channel group types
+channel-group-type.somneo.audio.label = Audio Player
+channel-group-type.somneo.audio.description = Channels to control the audio player.
+channel-group-type.somneo.audio.channel.aux.label = AUX Input
+channel-group-type.somneo.audio.channel.aux.description = Turn the AUX input on or off.
+channel-group-type.somneo.audio.channel.radio.label = Radio Remote Control
+channel-group-type.somneo.audio.channel.radio.description = Remote control for controlling the radio and seeking for a frequency.
+channel-group-type.somneo.light.label = Light
+channel-group-type.somneo.light.description = Different light channels.
+channel-group-type.somneo.sensor.label = Sensor Data
+channel-group-type.somneo.sensor.description = Data from the various sensors.
+channel-group-type.somneo.relax.label = Relax Breathe
+channel-group-type.somneo.relax.description = Light-guided breathing helps you relax for sleep.
+channel-group-type.somneo.relax.channel.lightIntensity.label = Light Intensity
+channel-group-type.somneo.relax.channel.lightIntensity.description = The channel allows to set the light intensity.
+channel-group-type.somneo.relax.channel.switch.label = Relax Breathe Program
+channel-group-type.somneo.relax.channel.switch.description = The switch channel allows to turn the relax breathe program on or off.
+channel-group-type.somneo.relax.channel.volume.label = Volume
+channel-group-type.somneo.relax.channel.volume.description = Set the volume of the relax breath program.
+channel-group-type.somneo.sunset.label = Sunset
+channel-group-type.somneo.sunset.description = Simulate a sunset with selectable lights and sounds.
+channel-group-type.somneo.sunset.channel.lightIntensity.label = Light Intensity
+channel-group-type.somneo.sunset.channel.lightIntensity.description = The channel allows to set the light intensity.
+channel-group-type.somneo.sunset.channel.switch.label = Sunset Program
+channel-group-type.somneo.sunset.channel.switch.description = The switch channel allows to turn the sunset program on or off.
+channel-group-type.somneo.sunset.channel.volume.label = Volume
+channel-group-type.somneo.sunset.channel.volume.description = Set the volume of the sunset program.
+
+# channel types
+channel-type.somneo.ambientNoise.label = Ambient Noise
+channel-type.somneo.ambientNoise.description = Ambient noise played during the sunset.
+channel-type.somneo.ambientNoise.state.option.off = No sound
+channel-type.somneo.ambientNoise.state.option.dus-1 = Soft Rain
+channel-type.somneo.ambientNoise.state.option.dus-2 = Ocean Waves
+channel-type.somneo.ambientNoise.state.option.dus-3 = Under Water
+channel-type.somneo.ambientNoise.state.option.dus-4 = Summer Lake
+channel-type.somneo.ambientNoise.state.option.fmr-1 = FM 1
+channel-type.somneo.ambientNoise.state.option.fmr-2 = FM 2
+channel-type.somneo.ambientNoise.state.option.fmr-3 = FM 3
+channel-type.somneo.ambientNoise.state.option.fmr-4 = FM 4
+channel-type.somneo.ambientNoise.state.option.fmr-5 = FM 5
+channel-type.somneo.breathingRate.label = Breathing Rate
+channel-type.somneo.breathingRate.description = Breathing rate per minute.
+channel-type.somneo.breathingRate.state.option.1 = 4 BR/min
+channel-type.somneo.breathingRate.state.option.2 = 5 BR/min
+channel-type.somneo.breathingRate.state.option.3 = 6 BR/min
+channel-type.somneo.breathingRate.state.option.4 = 7 BR/min
+channel-type.somneo.breathingRate.state.option.5 = 8 BR/min
+channel-type.somneo.breathingRate.state.option.6 = 9 BR/min
+channel-type.somneo.breathingRate.state.option.7 = 10 BR/min
+channel-type.somneo.colorSchema.label = Color Schema
+channel-type.somneo.colorSchema.description = Choose a personal sunset.
+channel-type.somneo.colorSchema.state.option.0 = Sunny day
+channel-type.somneo.colorSchema.state.option.1 = Island red
+channel-type.somneo.colorSchema.state.option.2 = Nordic white
+channel-type.somneo.colorSchema.state.option.3 = Caribbean red
+channel-type.somneo.frequency.label = Frequency
+channel-type.somneo.frequency.description = The currently selected radio frequency.
+channel-type.somneo.guidanceType.label = Breath Guidance Type
+channel-type.somneo.guidanceType.description = Select a breath guidance type.
+channel-type.somneo.guidanceType.state.option.0 = Light
+channel-type.somneo.guidanceType.state.option.1 = Sound
+channel-type.somneo.humidity.label = Humidity
+channel-type.somneo.humidity.description = The current humidity in %.
+channel-type.somneo.illuminance.label = Illuminance
+channel-type.somneo.illuminance.description = The current illuminance in lux.
+channel-type.somneo.light.label = Night Light
+channel-type.somneo.light.description = The light switch channel allows to turn the night light on or off.
+channel-type.somneo.noise.label = Noise
+channel-type.somneo.noise.description = The current noise in dB.
+channel-type.somneo.preset.label = FM Preset
+channel-type.somneo.preset.description = The Device has 5 presets to store radio frequencies.
+channel-type.somneo.relaxDuration.label = Duration
+channel-type.somneo.relaxDuration.description = The duration of relax breathe program in minutes.
+channel-type.somneo.relaxDuration.state.option.5 = 5 Minutes
+channel-type.somneo.relaxDuration.state.option.10 = 10 Minutes
+channel-type.somneo.relaxDuration.state.option.15 = 15 Minutes
+channel-type.somneo.remainingTime.label = Remaining Time
+channel-type.somneo.remainingTime.description = Remaining time from an activated program.
+channel-type.somneo.sunsetDuration.label = Duration
+channel-type.somneo.sunsetDuration.description = The duration of sunset program in minutes.
+channel-type.somneo.sunsetDuration.state.option.5 = 5 Minutes
+channel-type.somneo.sunsetDuration.state.option.10 = 10 Minutes
+channel-type.somneo.sunsetDuration.state.option.15 = 15 Minutes
+channel-type.somneo.sunsetDuration.state.option.20 = 20 Minutes
+channel-type.somneo.sunsetDuration.state.option.30 = 30 Minutes
+channel-type.somneo.sunsetDuration.state.option.45 = 45 Minutes
+channel-type.somneo.sunsetDuration.state.option.60 = 60 Minutes
+channel-type.somneo.temperature.label = Temperature
+channel-type.somneo.temperature.description = The current temperature in °C.
diff --git a/bundles/org.openhab.binding.somneo/src/main/resources/OH-INF/i18n/somneo_de.properties b/bundles/org.openhab.binding.somneo/src/main/resources/OH-INF/i18n/somneo_de.properties
new file mode 100644 (file)
index 0000000..bef4857
--- /dev/null
@@ -0,0 +1,108 @@
+# binding
+binding.somneo.name = Somneo Binding
+binding.somneo.description = Dieses Binding integriert Philips Somneo Geräte.
+
+# thing types
+thing-type.somneo.hf367x.label = Philips Somneo Light
+thing-type.somneo.hf367x.description = Ein intelligentes Schlaf- und Wecklicht mit Sensoren.
+
+# thing type config description
+thing-type.config.somneo.hf367x.hostname.label = Hostname
+thing-type.config.somneo.hf367x.hostname.description = Hostname oder IP-Adresse des Geräts.
+thing-type.config.somneo.hf367x.ignoreSSLErrors.label = SSL-Fehler ignorieren
+thing-type.config.somneo.hf367x.ignoreSSLErrors.description = Bei der Einstellung true werden Fehler bei ungültigen SSL-Zertifikaten ignoriert. Dies ist potenziell gefährlich.
+thing-type.config.somneo.hf367x.port.label = HTTP-Port
+thing-type.config.somneo.hf367x.port.description = HTTP-Port für Kommunikation. Sollte normalerweise nicht geändert werden.
+thing-type.config.somneo.hf367x.refreshInterval.label = Aktualisierungsintervall
+thing-type.config.somneo.hf367x.refreshInterval.description = Intervall in Sekunden, in dem das Gerät abgefragt wird.
+
+# channel group types
+channel-group-type.somneo.audio.label = Audioplayer
+channel-group-type.somneo.audio.description = Kanäle zur Steuerung des Audioplayers.
+channel-group-type.somneo.audio.channel.aux.label = AUX-Eingang
+channel-group-type.somneo.audio.channel.aux.description = Schaltet den AUX-Eingang ein oder aus.
+channel-group-type.somneo.audio.channel.radio.label = Radio Fernbedienung
+channel-group-type.somneo.audio.channel.radio.description = Fernbedienung zur Steuerung des Radios und zur Frequenzsuche.
+channel-group-type.somneo.light.label = Licht
+channel-group-type.somneo.light.description = Verschiedene Lichtkanäle.
+channel-group-type.somneo.sensor.label = Sensor-Daten
+channel-group-type.somneo.sensor.description = Daten von den verschiedenen Sensoren.
+channel-group-type.somneo.relax.label = Relax Breathe
+channel-group-type.somneo.relax.description = Pulsierende Lichteffekte entspannen die Atmung und unterstützen das Einschlafen.
+channel-group-type.somneo.relax.channel.lightIntensity.label = Lichtintensität
+channel-group-type.somneo.relax.channel.lightIntensity.description = Der Kanal ermöglicht die Einstellung der Lichtintensität.
+channel-group-type.somneo.relax.channel.switch.label = Relax Breathe Programm
+channel-group-type.somneo.relax.channel.switch.description = Mit dem Schaltkanal kann das Relax Breathe Programm ein- oder ausschalten werden.
+channel-group-type.somneo.relax.channel.volume.label = Lautstärke
+channel-group-type.somneo.relax.channel.volume.description = Stellen Sie die Lautstärke des Relax Breathe Programm ein.
+channel-group-type.somneo.sunset.label = Sonnenuntergang
+channel-group-type.somneo.sunset.description = Simulieren einen Sonnenuntergang mit wählbaren Lichtern und Geräuschen.
+channel-group-type.somneo.sunset.channel.lightIntensity.label = Lichtintensität
+channel-group-type.somneo.sunset.channel.lightIntensity.description = Der Kanal ermöglicht die Einstellung der Lichtintensität.
+channel-group-type.somneo.sunset.channel.switch.label = Sonnenuntergang Programm
+channel-group-type.somneo.sunset.channel.switch.description = Mit dem Schaltkanal kann das Sonnenuntergangsprogramm ein- oder ausschalten werden.
+channel-group-type.somneo.sunset.channel.volume.label = Lautstärke
+channel-group-type.somneo.sunset.channel.volume.description = Stellen Sie die Lautstärke des Sonnenuntergangsprogramms ein.
+
+# channel types
+channel-type.somneo.ambientNoise.label = Umgebungsgeräusche
+channel-type.somneo.ambientNoise.description = Umgebungsgeräusche, die während des Sonnenuntergangs abgespielt werden.
+channel-type.somneo.ambientNoise.state.option.off = Kein Geräusch
+channel-type.somneo.ambientNoise.state.option.dus-1 = Soft Rain
+channel-type.somneo.ambientNoise.state.option.dus-2 = Ocean Waves
+channel-type.somneo.ambientNoise.state.option.dus-3 = Under Water
+channel-type.somneo.ambientNoise.state.option.dus-4 = Summer Lake
+channel-type.somneo.ambientNoise.state.option.fmr-1 = FM 1
+channel-type.somneo.ambientNoise.state.option.fmr-2 = FM 2
+channel-type.somneo.ambientNoise.state.option.fmr-3 = FM 3
+channel-type.somneo.ambientNoise.state.option.fmr-4 = FM 4
+channel-type.somneo.ambientNoise.state.option.fmr-5 = FM 5
+channel-type.somneo.breathingRate.label = Atemzüge
+channel-type.somneo.breathingRate.description = Atemzüge pro Minute.
+channel-type.somneo.breathingRate.state.option.1 = 4 AZ/min
+channel-type.somneo.breathingRate.state.option.2 = 5 AZ/min
+channel-type.somneo.breathingRate.state.option.3 = 6 AZ/min
+channel-type.somneo.breathingRate.state.option.4 = 7 AZ/min
+channel-type.somneo.breathingRate.state.option.5 = 8 AZ/min
+channel-type.somneo.breathingRate.state.option.6 = 9 AZ/min
+channel-type.somneo.breathingRate.state.option.7 = 10 AZ/min
+channel-type.somneo.colorSchema.label = Farbschema
+channel-type.somneo.colorSchema.description = Wählen einen persönlichen Sonnenuntergang.
+channel-type.somneo.colorSchema.state.option.0 = Sunny day
+channel-type.somneo.colorSchema.state.option.1 = Island red
+channel-type.somneo.colorSchema.state.option.2 = Nordic white
+channel-type.somneo.colorSchema.state.option.3 = Caribbean red
+channel-type.somneo.frequency.label = Frequenz
+channel-type.somneo.frequency.description = Die aktuell gewählte Frequenz.
+channel-type.somneo.guidanceType.label = Atemzugführung Art
+channel-type.somneo.guidanceType.description = Wähle einen Art der Atemzuführung aus.
+channel-type.somneo.guidanceType.state.option.0 = Licht
+channel-type.somneo.guidanceType.state.option.1 = Sound
+channel-type.somneo.humidity.label = Luftfeuchtigkeit
+channel-type.somneo.humidity.description = Die aktuelle Luftfeuchtigkeit in %.
+channel-type.somneo.illuminance.label = Beleuchtungsstärke
+channel-type.somneo.illuminance.description = Die aktuelle Beleuchtungsstärke in Lux.
+channel-type.somneo.light.label = Nachtlicht
+channel-type.somneo.light.description = Mit dem Lichtschalterkanal kann das Nachtlicht ein- und ausgeschaltet werden.
+channel-type.somneo.noise.label = Lärm
+channel-type.somneo.noise.description = Der aktuelle Lärm in dB.
+channel-type.somneo.preset.label = FM-Voreinstellung
+channel-type.somneo.preset.description = Das Gerät verfügt über 5 Voreinstellung zum Speichern von Frequenzen.
+channel-type.somneo.relaxDuration.label = Dauer
+channel-type.somneo.relaxDuration.description = Die Dauer des Relax Breathe Programm in Minuten.
+channel-type.somneo.relaxDuration.state.option.5 = 5 Minuten
+channel-type.somneo.relaxDuration.state.option.10 = 10 Minuten
+channel-type.somneo.relaxDuration.state.option.15 = 15 Minuten
+channel-type.somneo.remainingTime.label = Verbleibende Zeit
+channel-type.somneo.remainingTime.description = Verbleibende Zeit eines aktivierten Programms.
+channel-type.somneo.sunsetDuration.label = Dauer
+channel-type.somneo.sunsetDuration.description = Die Dauer des Sonnenuntergangsprogramms in Minuten.
+channel-type.somneo.sunsetDuration.state.option.5 = 5 Minuten
+channel-type.somneo.sunsetDuration.state.option.10 = 10 Minuten
+channel-type.somneo.sunsetDuration.state.option.15 = 15 Minuten
+channel-type.somneo.sunsetDuration.state.option.20 = 20 Minuten
+channel-type.somneo.sunsetDuration.state.option.30 = 30 Minuten
+channel-type.somneo.sunsetDuration.state.option.45 = 45 Minuten
+channel-type.somneo.sunsetDuration.state.option.60 = 60 Minuten
+channel-type.somneo.temperature.label = Temperatur
+channel-type.somneo.temperature.description = Die aktuelle Temperatur in °C.
diff --git a/bundles/org.openhab.binding.somneo/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.somneo/src/main/resources/OH-INF/thing/thing-types.xml
new file mode 100644 (file)
index 0000000..56a20c4
--- /dev/null
@@ -0,0 +1,338 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<thing:thing-descriptions bindingId="somneo"
+       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="hf367x">
+               <label>Philips Somneo Light</label>
+               <description>A smart sleep and wake-up light with sensors.</description>
+
+               <channel-groups>
+                       <channel-group id="sensor" typeId="sensor"/>
+                       <channel-group id="light" typeId="light"/>
+                       <channel-group id="sunset" typeId="sunset"/>
+                       <channel-group id="relax" typeId="relax"/>
+                       <channel-group id="audio" typeId="audio"/>
+               </channel-groups>
+
+               <config-description>
+                       <parameter name="hostname" type="text" required="true">
+                               <context>network-address</context>
+                               <label>Hostname</label>
+                               <description>Hostname or IP address of the device.</description>
+                       </parameter>
+                       <parameter name="port" type="integer">
+                               <label>HTTP Port</label>
+                               <description>HTTP Port used for communication. Normally shouldn't be changed.</description>
+                               <default>443</default>
+                               <advanced>true</advanced>
+                       </parameter>
+                       <parameter name="refreshInterval" type="integer" unit="s" min="1">
+                               <label>Refresh Interval</label>
+                               <description>Interval the device is polled in sec.</description>
+                               <default>30</default>
+                               <advanced>true</advanced>
+                       </parameter>
+                       <parameter name="ignoreSSLErrors" type="boolean">
+                               <label>Ignore SSL Errors</label>
+                               <description>If set to true ignores invalid SSL certificate errors. This is potentially dangerous.</description>
+                               <default>true</default>
+                               <advanced>true</advanced>
+                       </parameter>
+               </config-description>
+       </thing-type>
+
+       <channel-group-type id="audio">
+               <label>Audio Player</label>
+               <description>Channels to control the audio player.</description>
+               <category>Player</category>
+               <channels>
+                       <channel id="radio" typeId="system.media-control">
+                               <label>Radio Remote Control</label>
+                               <description>Remote control for controlling the radio and seeking for a frequency.</description>
+                       </channel>
+                       <channel id="aux" typeId="system.power">
+                               <label>AUX Input</label>
+                               <description>Turn the AUX input on or off.</description>
+                       </channel>
+                       <channel id="volume" typeId="system.volume"/>
+                       <channel id="preset" typeId="preset"/>
+                       <channel id="frequency" typeId="frequency"></channel>
+               </channels>
+       </channel-group-type>
+
+       <channel-group-type id="light">
+               <label>Light</label>
+               <description>Different light channels.</description>
+               <category>Light</category>
+               <channels>
+                       <channel id="main" typeId="system.brightness"/>
+                       <channel id="night" typeId="light"/>
+               </channels>
+       </channel-group-type>
+
+       <channel-group-type id="sensor">
+               <label>Sensor Data</label>
+               <description>Data from the various sensors.</description>
+               <category>Sensor</category>
+               <channels>
+                       <channel id="illuminance" typeId="illuminance"/>
+                       <channel id="temperature" typeId="temperature"/>
+                       <channel id="humidity" typeId="humidity"/>
+                       <channel id="noise" typeId="noise"/>
+               </channels>
+       </channel-group-type>
+
+       <channel-group-type id="relax">
+               <label>Relax Breathe</label>
+               <description>Light-guided breathing helps you relax for sleep.</description>
+               <channels>
+                       <channel id="switch" typeId="system.power">
+                               <label>Relax Breathe Program</label>
+                               <description>The switch channel allows to turn the relax breathe program on or off.</description>
+                       </channel>
+                       <channel id="remainingTime" typeId="remainingTime"/>
+                       <channel id="breathingRate" typeId="breathingRate"/>
+                       <channel id="duration" typeId="relaxDuration"/>
+                       <channel id="guidanceType" typeId="guidanceType"/>
+                       <channel id="lightIntensity" typeId="system.brightness">
+                               <label>Light Intensity</label>
+                               <description>The channel allows to set the light intensity.</description>
+                       </channel>
+                       <channel id="volume" typeId="system.volume"/>
+               </channels>
+       </channel-group-type>
+
+       <channel-group-type id="sunset">
+               <label>Sunset</label>
+               <description>Simulate a sunset with selectable lights and sounds.</description>
+               <category>Sunset</category>
+               <channels>
+                       <channel id="switch" typeId="system.power">
+                               <label>Sunset Program</label>
+                               <description>The switch channel allows to turn the sunset program on or off.</description>
+                       </channel>
+                       <channel id="remainingTime" typeId="remainingTime"/>
+                       <channel id="lightIntensity" typeId="system.brightness">
+                               <label>Light Intensity</label>
+                               <description>The channel allows to set the light intensity.</description>
+                       </channel>
+                       <channel id="duration" typeId="sunsetDuration"/>
+                       <channel id="colorSchema" typeId="colorSchema"/>
+                       <channel id="ambientNoise" typeId="ambientNoise"/>
+                       <channel id="volume" typeId="system.volume"/>
+               </channels>
+       </channel-group-type>
+
+       <channel-type id="ambientNoise">
+               <item-type>String</item-type>
+               <label>Ambient Noise</label>
+               <description>Ambient noise played during the sunset.</description>
+               <category>Noise</category>
+               <tags>
+                       <tag>Control</tag>
+                       <tag>Noise</tag>
+               </tags>
+               <state>
+                       <options>
+                               <option value="off">No sound</option>
+                               <option value="dus-1">Soft Rain</option>
+                               <option value="dus-2">Ocean Waves</option>
+                               <option value="dus-3">Under Water</option>
+                               <option value="dus-4">Summer Lake</option>
+                               <option value="fmr-1">FM 1</option>
+                               <option value="fmr-2">FM 2</option>
+                               <option value="fmr-3">FM 3</option>
+                               <option value="fmr-4">FM 4</option>
+                               <option value="fmr-5">FM 5</option>
+                       </options>
+               </state>
+       </channel-type>
+
+       <channel-type id="breathingRate">
+               <item-type>Number</item-type>
+               <label>Breathing Rate</label>
+               <description>Breathing rate per minute.</description>
+               <tags>
+                       <tag>Control</tag>
+               </tags>
+               <state>
+                       <options>
+                               <option value="1">4 BR/min</option>
+                               <option value="2">5 BR/min</option>
+                               <option value="3">6 BR/min</option>
+                               <option value="4">7 BR/min</option>
+                               <option value="5">8 BR/min</option>
+                               <option value="6">9 BR/min</option>
+                               <option value="7">10 BR/min</option>
+                       </options>
+               </state>
+       </channel-type>
+
+       <channel-type id="colorSchema">
+               <item-type>Number</item-type>
+               <label>Color Schema</label>
+               <description>Choose a personal sunset.</description>
+               <category>Sunset</category>
+               <tags>
+                       <tag>Control</tag>
+                       <tag>ColorTemperature</tag>
+               </tags>
+               <state>
+                       <options>
+                               <option value="0">Sunny day</option>
+                               <option value="1">Island red</option>
+                               <option value="2">Nordic white</option>
+                               <option value="3">Caribbean red</option>
+                       </options>
+               </state>
+       </channel-type>
+
+       <channel-type id="frequency">
+               <item-type>String</item-type>
+               <label>Frequency</label>
+               <description>The currently selected radio frequency.</description>
+               <tags>
+                       <tag>Status</tag>
+               </tags>
+               <state readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="guidanceType">
+               <item-type>Number</item-type>
+               <label>Breath Guidance Type</label>
+               <description>Select a breath guidance type.</description>
+               <tags>
+                       <tag>Control</tag>
+               </tags>
+               <state>
+                       <options>
+                               <option value="0">Light</option>
+                               <option value="1">Sound</option>
+                       </options>
+               </state>
+       </channel-type>
+
+       <channel-type id="humidity">
+               <item-type>Number:Dimensionless</item-type>
+               <label>Humidity</label>
+               <description>The current humidity in %.</description>
+               <category>Humidity</category>
+               <tags>
+                       <tag>Measurement</tag>
+                       <tag>Humidity</tag>
+               </tags>
+               <state pattern="%.1f %unit%" readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="illuminance">
+               <item-type>Number:Illuminance</item-type>
+               <label>Illuminance</label>
+               <description>The current illuminance in lux.</description>
+               <category>Sun</category>
+               <tags>
+                       <tag>Measurement</tag>
+                       <tag>Light</tag>
+               </tags>
+               <state pattern="%.1f %unit%" readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="light">
+               <item-type>Switch</item-type>
+               <label>Night Light</label>
+               <description>The light switch channel allows to turn the night light on or off.</description>
+               <category>Light</category>
+               <tags>
+                       <tag>Control</tag>
+                       <tag>Light</tag>
+               </tags>
+       </channel-type>
+
+       <channel-type id="noise">
+               <item-type>Number:Dimensionless</item-type>
+               <label>Noise</label>
+               <description>The current noise in dB.</description>
+               <category>Noise</category>
+               <tags>
+                       <tag>Measurement</tag>
+                       <tag>Noise</tag>
+               </tags>
+               <state pattern="%d %unit%" readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="preset">
+               <item-type>String</item-type>
+               <label>FM Preset</label>
+               <description>The Device has 5 presets to store radio frequencies.</description>
+               <tags>
+                       <tag>Control</tag>
+               </tags>
+               <state/>
+       </channel-type>
+
+       <channel-type id="relaxDuration">
+               <item-type>Number:Time</item-type>
+               <label>Duration</label>
+               <description>The duration of relax breathe program in minutes.</description>
+               <category>Time</category>
+               <tags>
+                       <tag>Control</tag>
+                       <tag>Duration</tag>
+               </tags>
+               <state>
+                       <options>
+                               <option value="5">5 Minutes</option>
+                               <option value="10">10 Minutes</option>
+                               <option value="15">15 Minutes</option>
+                       </options>
+               </state>
+       </channel-type>
+
+       <channel-type id="remainingTime">
+               <item-type>Number:Time</item-type>
+               <label>Remaining Time</label>
+               <description>Remaining time from an activated program.</description>
+               <category>Time</category>
+               <tags>
+                       <tag>Status</tag>
+                       <tag>Duration</tag>
+               </tags>
+               <state pattern="%1$tM:%1$tS min" readOnly="true"/>
+       </channel-type>
+
+       <channel-type id="sunsetDuration">
+               <item-type>Number:Time</item-type>
+               <label>Duration</label>
+               <description>The duration of sunset program in minutes.</description>
+               <category>Time</category>
+               <tags>
+                       <tag>Control</tag>
+                       <tag>Duration</tag>
+               </tags>
+               <state>
+                       <options>
+                               <option value="5">5 Minutes</option>
+                               <option value="10">10 Minutes</option>
+                               <option value="15">15 Minutes</option>
+                               <option value="20">20 Minutes</option>
+                               <option value="30">30 Minutes</option>
+                               <option value="45">45 Minutes</option>
+                               <option value="60">60 Minutes</option>
+                       </options>
+               </state>
+       </channel-type>
+
+       <channel-type id="temperature">
+               <item-type>Number:Temperature</item-type>
+               <label>Temperature</label>
+               <description>The current temperature in °C.</description>
+               <category>Temperature</category>
+               <tags>
+                       <tag>Measurement</tag>
+                       <tag>Temperature</tag>
+               </tags>
+               <state pattern="%.1f %unit%" readOnly="true"/>
+       </channel-type>
+
+</thing:thing-descriptions>
index 5ec620b44d42d8bdb637bba8306109fbf24100da..2df26d5a3881bf5c899a916217f25cc54641c5df 100644 (file)
     <module>org.openhab.binding.solarwatt</module>
     <module>org.openhab.binding.somfymylink</module>
     <module>org.openhab.binding.somfytahoma</module>
+    <module>org.openhab.binding.somneo</module>
     <module>org.openhab.binding.sonnen</module>
     <module>org.openhab.binding.sonos</module>
     <module>org.openhab.binding.sonyaudio</module>