diff --git a/CODEOWNERS b/CODEOWNERS
index 64ab31aa3..7d4e10888 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -303,6 +303,7 @@
/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
diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml
index 8cddb09a3..688265456 100644
--- a/bom/openhab-addons/pom.xml
+++ b/bom/openhab-addons/pom.xml
@@ -1511,6 +1511,11 @@
org.openhab.binding.somfytahoma
${project.version}
+
+ org.openhab.addons.bundles
+ org.openhab.binding.somneo
+ ${project.version}
+
org.openhab.addons.bundles
org.openhab.binding.sonnen
diff --git a/bundles/org.openhab.binding.somneo/NOTICE b/bundles/org.openhab.binding.somneo/NOTICE
new file mode 100644
index 000000000..38d625e34
--- /dev/null
+++ b/bundles/org.openhab.binding.somneo/NOTICE
@@ -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
index 000000000..70cca0fe5
--- /dev/null
+++ b/bundles/org.openhab.binding.somneo/README.md
@@ -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" ["Measurement", "Light"] { channel="somneo:hf367x:1:sensor#illuminance" }
+Number:Temperature PhilipsSomneo_Temperature "Temperature" ["Measurement", "Temperature"] { channel="somneo:hf367x:1:sensor#temperature" }
+Number:Dimensionless PhilipsSomneo_Humidity "Humidity" ["Measurement", "Humidity"] { channel="somneo:hf367x:1:sensor#humidity" }
+Number:Dimensionless PhilipsSomneo_Noise "Noise" ["Measurement", "Noise"] { channel="somneo:hf367x:1:sensor#noise" }
+// Light
+Dimmer PhilipsSomneo_MainLight "Light" ["Control", "Light"] { channel="somneo:hf367x:1:light#main" }
+Switch PhilipsSomneo_NightLite "Night Light" ["Control", "Light"] { channel="somneo:hf367x:1:light#night" }
+// Sunset
+Switch PhilipsSomneo_SunsetSwitch "Sunset Program" ["Switch", "Power"] { channel="somneo:hf367x:1:sunset#switch" }
+Number:Time PhilipsSomneo_SunsetRemaining "Remaining Time" ["Status", "Duration"] { channel="somneo:hf367x:1:sunset#remainingTime" }
+Dimmer PhilipsSomneo_SunsetIntensity "Light Intensity" ["Control", "Light"] { channel="somneo:hf367x:1:sunset#lightIntensity" }
+Number:Time PhilipsSomneo_SunsetDuration "Duration" ["Control", "Duration"] { channel="somneo:hf367x:1:sunset#duration" }
+Number PhilipsSomneo_SunsetColor "Sunset Color" ["Control", "ColorTemperature"] { channel="somneo:hf367x:1:sunset#colorSchema" }
+String PhilipsSomneo_SunsetNoise "Ambient Noise" ["Control", "Noise"] { channel="somneo:hf367x:1:sunset#ambientNoise" }
+Dimmer PhilipsSomneo_SunsetVolume "Volume" ["Control", "SoundVolume"] { channel="somneo:hf367x:1:sunset#volume" }
+// Relax
+Switch PhilipsSomneo_RelaxSwitch "Relax Program" ["Switch", "Power"] { channel="somneo:hf367x:1:relax#switch" }
+Number:Time PhilipsSomneo_RelaxRemaining "Remaining 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" ["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" ["Control", "Light"] { channel="somneo:hf367x:1:relax#lightIntensity" }
+Dimmer PhilipsSomneo_RelaxVolume "Volume" ["Control", "SoundVolume"] { channel="somneo:hf367x:1:relax#volume" }
+// Audio
+Player PhilipsSomneo_AudioRadio "Radio Control" ["Control"] { channel="somneo:hf367x:1:audio#radio" }
+Switch PhilipsSomneo_AudioAux "AUX-Input" ["Switch", "Power"] { channel="somneo:hf367x:1:audio#aux" }
+Dimmer PhilipsSomneo_AudioVolume "Volume" ["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
index 000000000..aedb97c50
--- /dev/null
+++ b/bundles/org.openhab.binding.somneo/pom.xml
@@ -0,0 +1,17 @@
+
+
+
+ 4.0.0
+
+
+ org.openhab.addons.bundles
+ org.openhab.addons.reactor.bundles
+ 3.4.0-SNAPSHOT
+
+
+ org.openhab.binding.somneo
+
+ openHAB Add-ons :: Bundles :: Somneo Binding
+
+
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
index 000000000..108ecb059
--- /dev/null
+++ b/bundles/org.openhab.binding.somneo/src/main/feature/feature.xml
@@ -0,0 +1,9 @@
+
+
+ mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features
+
+
+ openhab-runtime-base
+ mvn:org.openhab.addons.bundles/org.openhab.binding.somneo/${project.version}
+
+
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
index 000000000..c839c6f83
--- /dev/null
+++ b/bundles/org.openhab.binding.somneo/src/main/java/org/openhab/binding/somneo/internal/HttpClientProvider.java
@@ -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
index 000000000..780b497d5
--- /dev/null
+++ b/bundles/org.openhab.binding.somneo/src/main/java/org/openhab/binding/somneo/internal/SomneoBindingConstants.java
@@ -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
index 000000000..c173e164e
--- /dev/null
+++ b/bundles/org.openhab.binding.somneo/src/main/java/org/openhab/binding/somneo/internal/SomneoConfiguration.java
@@ -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
index 000000000..39c74f8d9
--- /dev/null
+++ b/bundles/org.openhab.binding.somneo/src/main/java/org/openhab/binding/somneo/internal/SomneoHandler.java
@@ -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 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
index 000000000..66a9acd88
--- /dev/null
+++ b/bundles/org.openhab.binding.somneo/src/main/java/org/openhab/binding/somneo/internal/SomneoHandlerFactory.java
@@ -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 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
index 000000000..6bfa1d211
--- /dev/null
+++ b/bundles/org.openhab.binding.somneo/src/main/java/org/openhab/binding/somneo/internal/SomneoHttpConnector.java
@@ -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 executeUrl(String httpMethod, String endpoint, Class 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 url with the given httpMethod
+ *
+ * @param httpMethod the HTTP method to use
+ * @param endpoint the url endpoint
+ * @param content the content to be sent to the given url or
+ * null 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
index 000000000..42378f724
--- /dev/null
+++ b/bundles/org.openhab.binding.somneo/src/main/java/org/openhab/binding/somneo/internal/SomneoPresetStateDescriptionProvider.java
@@ -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
index 000000000..25fc821f4
--- /dev/null
+++ b/bundles/org.openhab.binding.somneo/src/main/java/org/openhab/binding/somneo/internal/model/AudioData.java
@@ -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
index 000000000..af25fceee
--- /dev/null
+++ b/bundles/org.openhab.binding.somneo/src/main/java/org/openhab/binding/somneo/internal/model/DeviceData.java
@@ -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
index 000000000..20172da8d
--- /dev/null
+++ b/bundles/org.openhab.binding.somneo/src/main/java/org/openhab/binding/somneo/internal/model/FirmwareData.java
@@ -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
index 000000000..a5adaac8d
--- /dev/null
+++ b/bundles/org.openhab.binding.somneo/src/main/java/org/openhab/binding/somneo/internal/model/LightData.java
@@ -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
index 000000000..381c10f1b
--- /dev/null
+++ b/bundles/org.openhab.binding.somneo/src/main/java/org/openhab/binding/somneo/internal/model/PresetData.java
@@ -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 createPresetOptions() {
+ List 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
index 000000000..a5068f1ca
--- /dev/null
+++ b/bundles/org.openhab.binding.somneo/src/main/java/org/openhab/binding/somneo/internal/model/RadioData.java
@@ -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
index 000000000..7ffb77b9f
--- /dev/null
+++ b/bundles/org.openhab.binding.somneo/src/main/java/org/openhab/binding/somneo/internal/model/RelaxData.java
@@ -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
index 000000000..dcdc6e5a3
--- /dev/null
+++ b/bundles/org.openhab.binding.somneo/src/main/java/org/openhab/binding/somneo/internal/model/SensorData.java
@@ -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
index 000000000..ae44dc5db
--- /dev/null
+++ b/bundles/org.openhab.binding.somneo/src/main/java/org/openhab/binding/somneo/internal/model/SunsetData.java
@@ -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
index 000000000..76cae077c
--- /dev/null
+++ b/bundles/org.openhab.binding.somneo/src/main/java/org/openhab/binding/somneo/internal/model/TimerData.java
@@ -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
index 000000000..886203395
--- /dev/null
+++ b/bundles/org.openhab.binding.somneo/src/main/java/org/openhab/binding/somneo/internal/model/WifiData.java
@@ -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
index 000000000..20342847b
--- /dev/null
+++ b/bundles/org.openhab.binding.somneo/src/main/resources/OH-INF/binding/binding.xml
@@ -0,0 +1,9 @@
+
+
+
+ Somneo Binding
+ This is the binding for Philips Somneo devices.
+
+
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
index 000000000..d5aa54476
--- /dev/null
+++ b/bundles/org.openhab.binding.somneo/src/main/resources/OH-INF/i18n/somneo.properties
@@ -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
index 000000000..bef485750
--- /dev/null
+++ b/bundles/org.openhab.binding.somneo/src/main/resources/OH-INF/i18n/somneo_de.properties
@@ -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
index 000000000..56a20c41d
--- /dev/null
+++ b/bundles/org.openhab.binding.somneo/src/main/resources/OH-INF/thing/thing-types.xml
@@ -0,0 +1,338 @@
+
+
+
+
+ Philips Somneo Light
+ A smart sleep and wake-up light with sensors.
+
+
+
+
+
+
+
+
+
+
+
+ network-address
+ Hostname
+ Hostname or IP address of the device.
+
+
+ HTTP Port
+ HTTP Port used for communication. Normally shouldn't be changed.
+ 443
+ true
+
+
+ Refresh Interval
+ Interval the device is polled in sec.
+ 30
+ true
+
+
+ Ignore SSL Errors
+ If set to true ignores invalid SSL certificate errors. This is potentially dangerous.
+ true
+ true
+
+
+
+
+
+ Audio Player
+ Channels to control the audio player.
+ Player
+
+
+ Radio Remote Control
+ Remote control for controlling the radio and seeking for a frequency.
+
+
+ AUX Input
+ Turn the AUX input on or off.
+
+
+
+
+
+
+
+
+ Light
+ Different light channels.
+ Light
+
+
+
+
+
+
+
+ Sensor Data
+ Data from the various sensors.
+ Sensor
+
+
+
+
+
+
+
+
+
+ Relax Breathe
+ Light-guided breathing helps you relax for sleep.
+
+
+ Relax Breathe Program
+ The switch channel allows to turn the relax breathe program on or off.
+
+
+
+
+
+
+ Light Intensity
+ The channel allows to set the light intensity.
+
+
+
+
+
+
+ Sunset
+ Simulate a sunset with selectable lights and sounds.
+ Sunset
+
+
+ Sunset Program
+ The switch channel allows to turn the sunset program on or off.
+
+
+
+ Light Intensity
+ The channel allows to set the light intensity.
+
+
+
+
+
+
+
+
+
+ String
+ Ambient Noise
+ Ambient noise played during the sunset.
+ Noise
+
+ Control
+ Noise
+
+
+
+ No sound
+ Soft Rain
+ Ocean Waves
+ Under Water
+ Summer Lake
+ FM 1
+ FM 2
+ FM 3
+ FM 4
+ FM 5
+
+
+
+
+
+ Number
+ Breathing Rate
+ Breathing rate per minute.
+
+ Control
+
+
+
+ 4 BR/min
+ 5 BR/min
+ 6 BR/min
+ 7 BR/min
+ 8 BR/min
+ 9 BR/min
+ 10 BR/min
+
+
+
+
+
+ Number
+ Color Schema
+ Choose a personal sunset.
+ Sunset
+
+ Control
+ ColorTemperature
+
+
+
+ Sunny day
+ Island red
+ Nordic white
+ Caribbean red
+
+
+
+
+
+ String
+ Frequency
+ The currently selected radio frequency.
+
+ Status
+
+
+
+
+
+ Number
+ Breath Guidance Type
+ Select a breath guidance type.
+
+ Control
+
+
+
+ Light
+ Sound
+
+
+
+
+
+ Number:Dimensionless
+ Humidity
+ The current humidity in %.
+ Humidity
+
+ Measurement
+ Humidity
+
+
+
+
+
+ Number:Illuminance
+ Illuminance
+ The current illuminance in lux.
+ Sun
+
+ Measurement
+ Light
+
+
+
+
+
+ Switch
+ Night Light
+ The light switch channel allows to turn the night light on or off.
+ Light
+
+ Control
+ Light
+
+
+
+
+ Number:Dimensionless
+ Noise
+ The current noise in dB.
+ Noise
+
+ Measurement
+ Noise
+
+
+
+
+
+ String
+ FM Preset
+ The Device has 5 presets to store radio frequencies.
+
+ Control
+
+
+
+
+
+ Number:Time
+ Duration
+ The duration of relax breathe program in minutes.
+ Time
+
+ Control
+ Duration
+
+
+
+ 5 Minutes
+ 10 Minutes
+ 15 Minutes
+
+
+
+
+
+ Number:Time
+ Remaining Time
+ Remaining time from an activated program.
+ Time
+
+ Status
+ Duration
+
+
+
+
+
+ Number:Time
+ Duration
+ The duration of sunset program in minutes.
+ Time
+
+ Control
+ Duration
+
+
+
+ 5 Minutes
+ 10 Minutes
+ 15 Minutes
+ 20 Minutes
+ 30 Minutes
+ 45 Minutes
+ 60 Minutes
+
+
+
+
+
+ Number:Temperature
+ Temperature
+ The current temperature in °C.
+ Temperature
+
+ Measurement
+ Temperature
+
+
+
+
+
diff --git a/bundles/pom.xml b/bundles/pom.xml
index 5ec620b44..2df26d5a3 100644
--- a/bundles/pom.xml
+++ b/bundles/pom.xml
@@ -336,6 +336,7 @@
org.openhab.binding.solarwatt
org.openhab.binding.somfymylink
org.openhab.binding.somfytahoma
+ org.openhab.binding.somneo
org.openhab.binding.sonnen
org.openhab.binding.sonos
org.openhab.binding.sonyaudio