diff --git a/bundles/org.openhab.binding.deconz/README.md b/bundles/org.openhab.binding.deconz/README.md index a4ce99e302a30..26d27b74d31fe 100644 --- a/bundles/org.openhab.binding.deconz/README.md +++ b/bundles/org.openhab.binding.deconz/README.md @@ -27,7 +27,8 @@ These sensors are supported: | deCONZ Artificial Daylight Sensor | deCONZ specific: simulated sensor | `daylightsensor` | | Carbon-Monoxide Sensor | ZHACarbonmonoxide | `carbonmonoxide` | -Additionally lights and window coverings (blinds) are supported: + +Additionally lights, window coverings (blinds) and thermostats are supported: | Device type | Resource Type | Thing type | |--------------------------------------|----------------------------------------|----------------------| @@ -36,6 +37,7 @@ Additionally lights and window coverings (blinds) are supported: | Color Light (w/o temperature) | Color dimmable light | `colorlight` | | Extended Color Light (w/temperature) | Extended color light | `extendedcolorlight` | | Blind / Window Covering | Window covering device | `windowcovering` | +| Thermostat | ZHAThermostat | `thermostat` | ## Discovery @@ -118,7 +120,7 @@ The sensor devices support some of the following channels: | light_level | Number | R | Current light level | lightsensor | | dark | Switch | R | Light level is below the darkness threshold | lightsensor, sometimes for presencesensor | | daylight | Switch | R | Light level is above the daylight threshold | lightsensor | -| temperature | Number:Temperature | R | Current temperature in ˚C | temperaturesensor, some Xiaomi sensors | +| temperature | Number:Temperature | R | Current temperature in ˚C | temperaturesensor, some Xiaomi sensors,thermostat| | humidity | Number:Dimensionless | R | Current humidity in % | humiditysensor | | pressure | Number:Pressure | R | Current pressure in hPa | pressuresensor | | open | Contact | R | Status of contacts: `OPEN`; `CLOSED` | openclosesensor | @@ -133,6 +135,7 @@ The sensor devices support some of the following channels: | battery_low | Switch | R | Battery level low: `ON`; `OFF` | any battery-powered sensor | | carbonmonoxide | Switch | R | `ON` = carbon monoxide detected | carbonmonoxide | + **NOTE:** Beside other non mandatory channels, the `battery_level` and `battery_low` channels will be added to the Thing during runtime if the sensor is battery-powered. The specification of your sensor depends on the deCONZ capabilities. Have a detailed look for [supported devices](https://github.com/dresden-elektronik/deconz-rest-plugin/wiki/Supported-Devices). @@ -146,6 +149,10 @@ Other devices support | color | Color | R/W | Color of an multi-color light | `colorlight`, `extendedcolorlight` | | color_temperature | Number | R/W | `0`->`100` represents cold -> warm | `colortemperaturelight`, `extendedcolorlight` | | position | Rollershutter | R/W | Position of the blind | `windowcovering` | +| heatsetpoint | Number:Temperature | R/W | Target Temperature in °C | `thermostat` | +| valve | Number:Dimensionless | R | Valve position in % | `thermostat` | +| mode | String | R/W | Mode: "auto", "heat" and "off" | `thermostat` | +| offset | Number | R | Temperature offset for sensor | `thermostat` | ### Trigger Channels diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/BindingConstants.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/BindingConstants.java index 24f851cbd2f63..76f761456bbbe 100644 --- a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/BindingConstants.java +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/BindingConstants.java @@ -48,6 +48,8 @@ public class BindingConstants { public static final ThingTypeUID THING_TYPE_BATTERY_SENSOR = new ThingTypeUID(BINDING_ID, "batterysensor"); public static final ThingTypeUID THING_TYPE_CARBONMONOXIDE_SENSOR = new ThingTypeUID(BINDING_ID, "carbonmonoxidesensor"); + // Special sensor - Thermostat + public static final ThingTypeUID THING_TYPE_THERMOSTAT = new ThingTypeUID(BINDING_ID, "thermostat"); // lights public static final ThingTypeUID THING_TYPE_ONOFF_LIGHT = new ThingTypeUID(BINDING_ID, "onofflight"); @@ -88,6 +90,10 @@ public class BindingConstants { public static final String CHANNEL_BATTERY_LEVEL = "battery_level"; public static final String CHANNEL_BATTERY_LOW = "battery_low"; public static final String CHANNEL_CARBONMONOXIDE = "carbonmonoxide"; + public static final String CHANNEL_HEATSETPOINT = "heatsetpoint"; + public static final String CHANNEL_THERMOSTAT_MODE = "mode"; + public static final String CHANNEL_TEMPERATURE_OFFSET = "offset"; + public static final String CHANNEL_VALVE_POSITION = "valve"; public static final String CHANNEL_SWITCH = "switch"; public static final String CHANNEL_BRIGHTNESS = "brightness"; diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/DeconzHandlerFactory.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/DeconzHandlerFactory.java index 8c6f28c1f0c84..7aa25d9e53a5b 100644 --- a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/DeconzHandlerFactory.java +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/DeconzHandlerFactory.java @@ -28,10 +28,13 @@ import org.eclipse.smarthome.io.net.http.WebSocketFactory; import org.openhab.binding.deconz.internal.handler.DeconzBridgeHandler; import org.openhab.binding.deconz.internal.handler.LightThingHandler; +import org.openhab.binding.deconz.internal.handler.SensorThermostatThingHandler; import org.openhab.binding.deconz.internal.handler.SensorThingHandler; import org.openhab.binding.deconz.internal.netutils.AsyncHttpClient; import org.openhab.binding.deconz.internal.types.LightType; import org.openhab.binding.deconz.internal.types.LightTypeDeserializer; +import org.openhab.binding.deconz.internal.types.ThermostatMode; +import org.openhab.binding.deconz.internal.types.ThermostatModeGsonTypeAdapter; import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Reference; @@ -50,7 +53,7 @@ public class DeconzHandlerFactory extends BaseThingHandlerFactory { private static final Set SUPPORTED_THING_TYPES_UIDS = Stream .of(DeconzBridgeHandler.SUPPORTED_THING_TYPES, LightThingHandler.SUPPORTED_THING_TYPE_UIDS, - SensorThingHandler.SUPPORTED_THING_TYPES) + SensorThingHandler.SUPPORTED_THING_TYPES, SensorThermostatThingHandler.SUPPORTED_THING_TYPES) .flatMap(Set::stream).collect(Collectors.toSet()); private final Gson gson; @@ -65,6 +68,7 @@ public DeconzHandlerFactory(final @Reference WebSocketFactory webSocketFactory, GsonBuilder gsonBuilder = new GsonBuilder(); gsonBuilder.registerTypeAdapter(LightType.class, new LightTypeDeserializer()); + gsonBuilder.registerTypeAdapter(ThermostatMode.class, new ThermostatModeGsonTypeAdapter()); gson = gsonBuilder.create(); } @@ -84,6 +88,8 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) { return new LightThingHandler(thing, gson); } else if (SensorThingHandler.SUPPORTED_THING_TYPES.contains(thingTypeUID)) { return new SensorThingHandler(thing, gson); + } else if (SensorThermostatThingHandler.SUPPORTED_THING_TYPES.contains(thingTypeUID)) { + return new SensorThermostatThingHandler(thing, gson); } return null; diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/discovery/ThingDiscoveryService.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/discovery/ThingDiscoveryService.java index c5a81e992867b..d275f7955da84 100644 --- a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/discovery/ThingDiscoveryService.java +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/discovery/ThingDiscoveryService.java @@ -35,6 +35,7 @@ import org.openhab.binding.deconz.internal.dto.SensorMessage; import org.openhab.binding.deconz.internal.handler.DeconzBridgeHandler; import org.openhab.binding.deconz.internal.handler.LightThingHandler; +import org.openhab.binding.deconz.internal.handler.SensorThermostatThingHandler; import org.openhab.binding.deconz.internal.handler.SensorThingHandler; import org.openhab.binding.deconz.internal.types.LightType; import org.slf4j.Logger; @@ -49,7 +50,8 @@ @NonNullByDefault public class ThingDiscoveryService extends AbstractDiscoveryService implements DiscoveryService, ThingHandlerService { private static final Set SUPPORTED_THING_TYPES_UIDS = Stream - .of(LightThingHandler.SUPPORTED_THING_TYPE_UIDS, SensorThingHandler.SUPPORTED_THING_TYPES) + .of(LightThingHandler.SUPPORTED_THING_TYPE_UIDS, SensorThingHandler.SUPPORTED_THING_TYPES, + SensorThermostatThingHandler.SUPPORTED_THING_TYPES) .flatMap(Set::stream).collect(Collectors.toSet()); private final Logger logger = LoggerFactory.getLogger(ThingDiscoveryService.class); @@ -193,6 +195,8 @@ private void addSensor(String sensorID, SensorMessage sensor) { thingTypeUID = THING_TYPE_VIBRATION_SENSOR; // ZHAVibration } else if (sensor.type.contains("ZHABattery")) { thingTypeUID = THING_TYPE_BATTERY_SENSOR; // ZHABattery + } else if (sensor.type.contains("ZHAThermostat")) { + thingTypeUID = THING_TYPE_THERMOSTAT; // ZHAThermostat } else { logger.debug("Unknown type {}", sensor.type); return; diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/SensorConfig.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/SensorConfig.java index cda5e008dc13a..82eee443fd72f 100644 --- a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/SensorConfig.java +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/SensorConfig.java @@ -14,6 +14,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.deconz.internal.types.ThermostatMode; /** * The {@link SensorConfig} is send by the the Rest API. @@ -23,6 +24,7 @@ * https://dresden-elektronik.github.io/deconz-rest-doc/sensors/. * * @author David Graeff - Initial contribution + * @author Lukas Agethen - Add Thermostat parameters */ @NonNullByDefault public class SensorConfig { @@ -30,4 +32,7 @@ public class SensorConfig { public boolean reachable = true; public @Nullable Integer battery; public @Nullable Float temperature; + public @Nullable Integer heatsetpoint; + public @Nullable ThermostatMode mode; + public @Nullable Integer offset; } diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/SensorState.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/SensorState.java index 0ec4cb3a21fd0..bd4f16153db03 100644 --- a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/SensorState.java +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/SensorState.java @@ -72,6 +72,8 @@ public class SensorState { public @Nullable Integer buttonevent; /** Switches may provide this value. */ public @Nullable Integer gesture; + /** Thermostat may provide this value. */ + public @Nullable Integer valve; /** deCONZ sends a last update string with every event. */ public @Nullable String lastupdated; diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/ThermostatConfig.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/ThermostatConfig.java new file mode 100644 index 0000000000000..529f72a616073 --- /dev/null +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/ThermostatConfig.java @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.deconz.internal.dto; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.deconz.internal.types.ThermostatMode; + +/** + * The {@link ThermostatConfig} is send to the Rest API to configure Thermostat. + * + * @author Lukas Agethen - Initial contribution + */ +@NonNullByDefault +public class ThermostatConfig { + public @Nullable Integer heatsetpoint; + public @Nullable ThermostatMode mode; + public @Nullable Integer offset; +} diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/SensorBaseThingHandler.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/SensorBaseThingHandler.java new file mode 100644 index 0000000000000..3a86375575874 --- /dev/null +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/SensorBaseThingHandler.java @@ -0,0 +1,299 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.deconz.internal.handler; + +import static org.openhab.binding.deconz.internal.BindingConstants.*; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.Map; + +import javax.measure.Unit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.library.types.DateTimeType; +import org.eclipse.smarthome.core.library.types.DecimalType; +import org.eclipse.smarthome.core.library.types.OnOffType; +import org.eclipse.smarthome.core.library.types.QuantityType; +import org.eclipse.smarthome.core.thing.Channel; +import org.eclipse.smarthome.core.thing.ChannelUID; +import org.eclipse.smarthome.core.thing.Thing; +import org.eclipse.smarthome.core.thing.ThingStatus; +import org.eclipse.smarthome.core.thing.ThingStatusDetail; +import org.eclipse.smarthome.core.thing.binding.ThingHandlerCallback; +import org.eclipse.smarthome.core.thing.type.ChannelKind; +import org.eclipse.smarthome.core.thing.type.ChannelTypeUID; +import org.eclipse.smarthome.core.types.Command; +import org.openhab.binding.deconz.internal.dto.DeconzBaseMessage; +import org.openhab.binding.deconz.internal.dto.SensorConfig; +import org.openhab.binding.deconz.internal.dto.SensorMessage; +import org.openhab.binding.deconz.internal.dto.SensorState; +import org.openhab.binding.deconz.internal.netutils.AsyncHttpClient; +import org.openhab.binding.deconz.internal.netutils.WebSocketConnection; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.Gson; + +/** + * This sensor Thing doesn't establish any connections, that is done by the bridge Thing. + * + * It waits for the bridge to come online, grab the websocket connection and bridge configuration + * and registers to the websocket connection as a listener. + * + * A REST API call is made to get the initial sensor state. + * + * Every sensor and switch is supported by this Thing, because a unified state is kept + * in {@link #sensorState}. Every field that got received by the REST API for this specific + * sensor is published to the framework. + * + * @author David Graeff - Initial contribution + * @author Lukas Agethen - Refactored to provide better extensibility + */ +@NonNullByDefault +public abstract class SensorBaseThingHandler extends DeconzBaseThingHandler { + private final Logger logger = LoggerFactory.getLogger(SensorBaseThingHandler.class); + /** + * The sensor state. Contains all possible fields for all supported sensors and switches + */ + protected SensorConfig sensorConfig = new SensorConfig(); + protected SensorState sensorState = new SensorState(); + /** + * Prevent a dispose/init cycle while this flag is set. Use for property updates + */ + private boolean ignoreConfigurationUpdate; + + public SensorBaseThingHandler(Thing thing, Gson gson) { + super(thing, gson); + } + + @Override + protected void requestState() { + requestState("sensors"); + } + + @Override + protected void registerListener() { + WebSocketConnection conn = connection; + if (conn != null) { + conn.registerSensorListener(config.id, this); + } + } + + @Override + protected void unregisterListener() { + WebSocketConnection conn = connection; + if (conn != null) { + conn.unregisterSensorListener(config.id); + } + } + + @Override + public abstract void handleCommand(ChannelUID channelUID, Command command); + + protected abstract void createTypeSpecificChannels(SensorConfig sensorState, SensorState sensorConfig); + + protected abstract List getConfigChannels(); + + @Override + public void handleConfigurationUpdate(Map configurationParameters) { + if (!ignoreConfigurationUpdate) { + super.handleConfigurationUpdate(configurationParameters); + } + } + + @Override + protected @Nullable SensorMessage parseStateResponse(AsyncHttpClient.Result r) { + if (r.getResponseCode() == 403) { + return null; + } else if (r.getResponseCode() == 200) { + return gson.fromJson(r.getBody(), SensorMessage.class); + } else { + throw new IllegalStateException("Unknown status code " + r.getResponseCode() + " for full state request"); + } + } + + @Override + protected void processStateResponse(@Nullable SensorMessage stateResponse) { + if (stateResponse == null) { + return; + } + SensorConfig newSensorConfig = stateResponse.config; + sensorConfig = newSensorConfig != null ? newSensorConfig : new SensorConfig(); + SensorState newSensorState = stateResponse.state; + sensorState = newSensorState != null ? newSensorState : new SensorState(); + + // Add some information about the sensor + if (!sensorConfig.reachable) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.GONE, "Not reachable"); + return; + } + + if (!sensorConfig.on) { + updateStatus(ThingStatus.OFFLINE); + return; + } + + Map editProperties = editProperties(); + editProperties.put(Thing.PROPERTY_FIRMWARE_VERSION, stateResponse.swversion); + editProperties.put(Thing.PROPERTY_MODEL_ID, stateResponse.modelid); + editProperties.put(UNIQUE_ID, stateResponse.uniqueid); + ignoreConfigurationUpdate = true; + updateProperties(editProperties); + + // Some sensors support optional channels + // (see https://github.com/dresden-elektronik/deconz-rest-plugin/wiki/Supported-Devices#sensors) + // any battery-powered sensor + if (sensorConfig.battery != null) { + createChannel(CHANNEL_BATTERY_LEVEL, ChannelKind.STATE); + createChannel(CHANNEL_BATTERY_LOW, ChannelKind.STATE); + } + + createTypeSpecificChannels(sensorConfig, sensorState); + + ignoreConfigurationUpdate = false; + + // Initial data + updateChannels(sensorConfig); + updateChannels(sensorState, true); + + updateStatus(ThingStatus.ONLINE); + } + + protected void createChannel(String channelId, ChannelKind kind) { + ThingHandlerCallback callback = getCallback(); + if (callback != null) { + ChannelUID channelUID = new ChannelUID(thing.getUID(), channelId); + ChannelTypeUID channelTypeUID; + switch (channelId) { + case CHANNEL_BATTERY_LEVEL: + channelTypeUID = new ChannelTypeUID("system:battery-level"); + break; + case CHANNEL_BATTERY_LOW: + channelTypeUID = new ChannelTypeUID("system:low-battery"); + break; + default: + channelTypeUID = new ChannelTypeUID(BINDING_ID, channelId); + break; + } + Channel channel = callback.createChannelBuilder(channelUID, channelTypeUID).withKind(kind).build(); + updateThing(editThing().withoutChannel(channelUID).withChannel(channel).build()); + } + } + + /** + * Update channel value from {@link SensorConfig} object - override to include further channels + * + * @param channelUID + * @param newConfig + */ + protected void valueUpdated(ChannelUID channelUID, SensorConfig newConfig) { + Integer batteryLevel = newConfig.battery; + switch (channelUID.getId()) { + case CHANNEL_BATTERY_LEVEL: + if (batteryLevel != null) { + updateState(channelUID, new DecimalType(batteryLevel.longValue())); + } + break; + case CHANNEL_BATTERY_LOW: + if (batteryLevel != null) { + updateState(channelUID, OnOffType.from(batteryLevel <= 10)); + } + break; + default: + // other cases covered by sub-class + } + } + + /** + * Update channel value from {@link SensorState} object - override to include further channels + * + * @param channelID + * @param newState + * @param initializing + */ + protected void valueUpdated(String channelID, SensorState newState, boolean initializing) { + switch (channelID) { + case CHANNEL_LAST_UPDATED: + String lastUpdated = newState.lastupdated; + if (lastUpdated != null && !"none".equals(lastUpdated)) { + updateState(channelID, + new DateTimeType(ZonedDateTime.ofInstant( + LocalDateTime.parse(lastUpdated, DateTimeFormatter.ISO_LOCAL_DATE_TIME), + ZoneOffset.UTC, ZoneId.systemDefault()))); + } + break; + default: + // other cases covered by sub-class + } + } + + @Override + public void messageReceived(String sensorID, DeconzBaseMessage message) { + if (message instanceof SensorMessage) { + SensorMessage sensorMessage = (SensorMessage) message; + SensorConfig sensorConfig = sensorMessage.config; + if (sensorConfig != null) { + this.sensorConfig = sensorConfig; + updateChannels(sensorConfig); + } + SensorState sensorState = sensorMessage.state; + if (sensorState != null) { + updateChannels(sensorState, false); + } + } + } + + private void updateChannels(SensorConfig newConfig) { + List configChannels = getConfigChannels(); + thing.getChannels().stream().map(Channel::getUID) + .filter(channelUID -> configChannels.contains(channelUID.getId())) + .forEach((channelUID) -> valueUpdated(channelUID, newConfig)); + } + + protected void updateChannels(SensorState newState, boolean initializing) { + logger.trace("{} received {}", thing.getUID(), newState); + sensorState = newState; + thing.getChannels().forEach(channel -> valueUpdated(channel.getUID().getId(), newState, initializing)); + } + + protected void updateSwitchChannel(String channelID, @Nullable Boolean value) { + if (value == null) { + return; + } + updateState(channelID, OnOffType.from(value)); + } + + protected void updateDecimalTypeChannel(String channelID, @Nullable Number value) { + if (value == null) { + return; + } + updateState(channelID, new DecimalType(value.longValue())); + } + + protected void updateQuantityTypeChannel(String channelID, @Nullable Number value, Unit unit) { + updateQuantityTypeChannel(channelID, value, unit, 1.0); + } + + protected void updateQuantityTypeChannel(String channelID, @Nullable Number value, Unit unit, double scaling) { + if (value == null) { + return; + } + updateState(channelID, new QuantityType<>(value.doubleValue() * scaling, unit)); + } +} diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/SensorThermostatThingHandler.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/SensorThermostatThingHandler.java new file mode 100644 index 0000000000000..9e57d485a6093 --- /dev/null +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/SensorThermostatThingHandler.java @@ -0,0 +1,199 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.deconz.internal.handler; + +import static org.eclipse.smarthome.core.library.unit.SIUnits.CELSIUS; +import static org.eclipse.smarthome.core.library.unit.SmartHomeUnits.PERCENT; +import static org.openhab.binding.deconz.internal.BindingConstants.*; +import static org.openhab.binding.deconz.internal.Util.buildUrl; + +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.smarthome.core.library.types.DecimalType; +import org.eclipse.smarthome.core.library.types.QuantityType; +import org.eclipse.smarthome.core.library.types.StringType; +import org.eclipse.smarthome.core.thing.ChannelUID; +import org.eclipse.smarthome.core.thing.Thing; +import org.eclipse.smarthome.core.thing.ThingTypeUID; +import org.eclipse.smarthome.core.types.Command; +import org.eclipse.smarthome.core.types.RefreshType; +import org.openhab.binding.deconz.internal.dto.SensorConfig; +import org.openhab.binding.deconz.internal.dto.SensorState; +import org.openhab.binding.deconz.internal.dto.ThermostatConfig; +import org.openhab.binding.deconz.internal.netutils.AsyncHttpClient; +import org.openhab.binding.deconz.internal.types.ThermostatMode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.Gson; + +/** + * This sensor Thermostat Thing doesn't establish any connections, that is done by the bridge Thing. + * + * It waits for the bridge to come online, grab the websocket connection and bridge configuration + * and registers to the websocket connection as a listener. + * + * A REST API call is made to get the initial sensor state. + * + * Only the Thermostat is supported by this Thing, because a unified state is kept + * in {@link #sensorState}. Every field that got received by the REST API for this specific + * sensor is published to the framework. + * + * @author Lukas Agethen - Initial contribution + */ +@NonNullByDefault +public class SensorThermostatThingHandler extends SensorBaseThingHandler { + public static final Set SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_THERMOSTAT); + + private static final List CONFIG_CHANNELS = Arrays.asList(CHANNEL_BATTERY_LEVEL, CHANNEL_BATTERY_LOW, + CHANNEL_HEATSETPOINT, CHANNEL_TEMPERATURE_OFFSET, CHANNEL_THERMOSTAT_MODE); + + private final Logger logger = LoggerFactory.getLogger(SensorThermostatThingHandler.class); + + public SensorThermostatThingHandler(Thing thing, Gson gson) { + super(thing, gson); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + if (command instanceof RefreshType) { + sensorState.buttonevent = null; + valueUpdated(channelUID.getId(), sensorState, false); + return; + } + ThermostatConfig newConfig = new ThermostatConfig(); + String channelId = channelUID.getId(); + switch (channelId) { + case CHANNEL_HEATSETPOINT: + Integer newHeatsetpoint = getTemperatureFromCommand(command); + if (newHeatsetpoint == null) { + logger.warn("Heatsetpoint must not be null."); + return; + } + newConfig.heatsetpoint = newHeatsetpoint; + break; + case CHANNEL_TEMPERATURE_OFFSET: + Integer newOffset = getTemperatureFromCommand(command); + if (newOffset == null) { + logger.warn("Offset must not be null."); + return; + } + newConfig.offset = newOffset; + break; + case CHANNEL_THERMOSTAT_MODE: + if (command instanceof StringType) { + String thermostatMode = ((StringType) command).toString(); + try { + newConfig.mode = ThermostatMode.valueOf(thermostatMode); + } catch (IllegalArgumentException ex) { + logger.warn("Invalid thermostat mode: {}. Valid values: {}", thermostatMode, + ThermostatMode.values()); + return; + } + if (newConfig.mode == ThermostatMode.UNKNOWN) { + logger.warn("Invalid thermostat mode: {}. Valid values: {}", thermostatMode, + ThermostatMode.values()); + return; + } + } else { + return; + } + break; + default: + // no supported command + return; + + } + + AsyncHttpClient asyncHttpClient = http; + if (asyncHttpClient == null) { + return; + } + String url = buildUrl(bridgeConfig.host, bridgeConfig.httpPort, bridgeConfig.apikey, "sensors", config.id, + "config"); + + String json = gson.toJson(newConfig); + logger.trace("Sending {} to sensor {} via {}", json, config.id, url); + asyncHttpClient.put(url, json, bridgeConfig.timeout).thenAccept(v -> { + String bodyContent = v.getBody(); + logger.trace("Result code={}, body={}", v.getResponseCode(), bodyContent); + if (!bodyContent.contains("success")) { + logger.debug("Sending command {} to channel {} failed: {}", command, channelUID, bodyContent); + } + + }).exceptionally(e -> { + logger.debug("Sending command {} to channel {} failed:", command, channelUID, e); + return null; + }); + } + + @Override + protected void valueUpdated(ChannelUID channelUID, SensorConfig newConfig) { + super.valueUpdated(channelUID, newConfig); + String mode = newConfig.mode != null ? newConfig.mode.name() : ThermostatMode.UNKNOWN.name(); + String channelID = channelUID.getId(); + switch (channelID) { + case CHANNEL_HEATSETPOINT: + updateQuantityTypeChannel(channelID, newConfig.heatsetpoint, CELSIUS, 1.0 / 100); + break; + case CHANNEL_TEMPERATURE_OFFSET: + updateQuantityTypeChannel(channelID, newConfig.offset, CELSIUS, 1.0 / 100); + break; + case CHANNEL_THERMOSTAT_MODE: + if (mode != null) { + updateState(channelUID, new StringType(mode)); + } + break; + } + } + + @Override + protected void valueUpdated(String channelID, SensorState newState, boolean initializing) { + super.valueUpdated(channelID, newState, initializing); + switch (channelID) { + case CHANNEL_TEMPERATURE: + updateQuantityTypeChannel(channelID, newState.temperature, CELSIUS, 1.0 / 100); + break; + case CHANNEL_VALVE_POSITION: + updateQuantityTypeChannel(channelID, newState.valve, PERCENT, 100.0 / 255); + break; + } + } + + @Override + protected void createTypeSpecificChannels(SensorConfig sensorConfig, SensorState sensorState) { + } + + @Override + protected List getConfigChannels() { + return CONFIG_CHANNELS; + } + + private @Nullable Integer getTemperatureFromCommand(Command command) { + BigDecimal newTemperature; + if (command instanceof DecimalType) { + newTemperature = ((DecimalType) command).toBigDecimal(); + } else if (command instanceof QuantityType) { + newTemperature = ((QuantityType) command).toUnit(CELSIUS).toBigDecimal(); + } else { + return null; + } + return newTemperature.scaleByPowerOfTen(2).intValue(); + } +} diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/SensorThingHandler.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/SensorThingHandler.java index d5f23f1961dad..1f06155bbc6c4 100644 --- a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/SensorThingHandler.java +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/SensorThingHandler.java @@ -12,53 +12,30 @@ */ package org.openhab.binding.deconz.internal.handler; -import static org.eclipse.smarthome.core.library.unit.MetricPrefix.HECTO; -import static org.eclipse.smarthome.core.library.unit.MetricPrefix.MILLI; -import static org.eclipse.smarthome.core.library.unit.SIUnits.CELSIUS; -import static org.eclipse.smarthome.core.library.unit.SIUnits.PASCAL; +import static org.eclipse.smarthome.core.library.unit.MetricPrefix.*; +import static org.eclipse.smarthome.core.library.unit.SIUnits.*; import static org.eclipse.smarthome.core.library.unit.SmartHomeUnits.*; import static org.openhab.binding.deconz.internal.BindingConstants.*; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.time.ZoneOffset; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; import java.util.Arrays; import java.util.Collections; import java.util.List; -import java.util.Map; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; -import javax.measure.Unit; - import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; -import org.eclipse.smarthome.core.library.types.DateTimeType; -import org.eclipse.smarthome.core.library.types.DecimalType; -import org.eclipse.smarthome.core.library.types.OnOffType; import org.eclipse.smarthome.core.library.types.OpenClosedType; import org.eclipse.smarthome.core.library.types.QuantityType; import org.eclipse.smarthome.core.library.types.StringType; -import org.eclipse.smarthome.core.thing.Channel; import org.eclipse.smarthome.core.thing.ChannelUID; import org.eclipse.smarthome.core.thing.Thing; -import org.eclipse.smarthome.core.thing.ThingStatus; -import org.eclipse.smarthome.core.thing.ThingStatusDetail; import org.eclipse.smarthome.core.thing.ThingTypeUID; -import org.eclipse.smarthome.core.thing.binding.ThingHandlerCallback; import org.eclipse.smarthome.core.thing.type.ChannelKind; -import org.eclipse.smarthome.core.thing.type.ChannelTypeUID; import org.eclipse.smarthome.core.types.Command; import org.eclipse.smarthome.core.types.RefreshType; -import org.openhab.binding.deconz.internal.dto.DeconzBaseMessage; import org.openhab.binding.deconz.internal.dto.SensorConfig; -import org.openhab.binding.deconz.internal.dto.SensorMessage; import org.openhab.binding.deconz.internal.dto.SensorState; -import org.openhab.binding.deconz.internal.netutils.AsyncHttpClient; -import org.openhab.binding.deconz.internal.netutils.WebSocketConnection; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -79,7 +56,7 @@ * @author David Graeff - Initial contribution */ @NonNullByDefault -public class SensorThingHandler extends DeconzBaseThingHandler { +public class SensorThingHandler extends SensorBaseThingHandler { public static final Set SUPPORTED_THING_TYPES = Collections .unmodifiableSet(Stream.of(THING_TYPE_PRESENCE_SENSOR, THING_TYPE_DAYLIGHT_SENSOR, THING_TYPE_POWER_SENSOR, THING_TYPE_CONSUMPTION_SENSOR, THING_TYPE_LIGHT_SENSOR, THING_TYPE_TEMPERATURE_SENSOR, @@ -92,41 +69,11 @@ public class SensorThingHandler extends DeconzBaseThingHandler { CHANNEL_TEMPERATURE); private final Logger logger = LoggerFactory.getLogger(SensorThingHandler.class); - /** - * The sensor state. Contains all possible fields for all supported sensors and switches - */ - private SensorConfig sensorConfig = new SensorConfig(); - private SensorState sensorState = new SensorState(); - /** - * Prevent a dispose/init cycle while this flag is set. Use for property updates - */ - private boolean ignoreConfigurationUpdate; public SensorThingHandler(Thing thing, Gson gson) { super(thing, gson); } - @Override - protected void requestState() { - requestState("sensors"); - } - - @Override - protected void registerListener() { - WebSocketConnection conn = connection; - if (conn != null) { - conn.registerSensorListener(config.id, this); - } - } - - @Override - protected void unregisterListener() { - WebSocketConnection conn = connection; - if (conn != null) { - conn.unregisterSensorListener(config.id); - } - } - @Override public void handleCommand(ChannelUID channelUID, Command command) { if (!(command instanceof RefreshType)) { @@ -138,137 +85,11 @@ public void handleCommand(ChannelUID channelUID, Command command) { } @Override - public void handleConfigurationUpdate(Map configurationParameters) { - if (!ignoreConfigurationUpdate) { - super.handleConfigurationUpdate(configurationParameters); - } - } - - @Override - protected @Nullable SensorMessage parseStateResponse(AsyncHttpClient.Result r) { - if (r.getResponseCode() == 403) { - return null; - } else if (r.getResponseCode() == 200) { - return gson.fromJson(r.getBody(), SensorMessage.class); - } else { - throw new IllegalStateException("Unknown status code " + r.getResponseCode() + " for full state request"); - } - } - - @Override - protected void processStateResponse(@Nullable SensorMessage stateResponse) { - if (stateResponse == null) { - return; - } - SensorConfig newSensorConfig = stateResponse.config; - sensorConfig = newSensorConfig != null ? newSensorConfig : new SensorConfig(); - SensorState newSensorState = stateResponse.state; - sensorState = newSensorState != null ? newSensorState : new SensorState(); - - // Add some information about the sensor - if (!sensorConfig.reachable) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.GONE, "Not reachable"); - return; - } - - if (!sensorConfig.on) { - updateStatus(ThingStatus.OFFLINE); - return; - } - - Map editProperties = editProperties(); - editProperties.put(Thing.PROPERTY_FIRMWARE_VERSION, stateResponse.swversion); - editProperties.put(Thing.PROPERTY_MODEL_ID, stateResponse.modelid); - editProperties.put(UNIQUE_ID, stateResponse.uniqueid); - ignoreConfigurationUpdate = true; - updateProperties(editProperties); - - // Some sensors support optional channels - // (see https://github.com/dresden-elektronik/deconz-rest-plugin/wiki/Supported-Devices#sensors) - // any battery-powered sensor - if (sensorConfig.battery != null) { - createChannel(CHANNEL_BATTERY_LEVEL, ChannelKind.STATE); - createChannel(CHANNEL_BATTERY_LOW, ChannelKind.STATE); - } - - // some Xiaomi sensors - if (sensorConfig.temperature != null) { - createChannel(CHANNEL_TEMPERATURE, ChannelKind.STATE); - } - - // ZHAPresence - e.g. IKEA TRÅDFRI motion sensor - if (sensorState.dark != null) { - createChannel(CHANNEL_DARK, ChannelKind.STATE); - } - - // ZHAConsumption - e.g Bitron 902010/25 or Heiman SmartPlug - if (sensorState.power != null) { - createChannel(CHANNEL_POWER, ChannelKind.STATE); - } - - // ZHAPower - e.g. Heiman SmartPlug - if (sensorState.voltage != null) { - createChannel(CHANNEL_VOLTAGE, ChannelKind.STATE); - } - if (sensorState.current != null) { - createChannel(CHANNEL_CURRENT, ChannelKind.STATE); - } - - // IAS Zone sensor - e.g. Heiman HS1MS motion sensor - if (sensorState.tampered != null) { - createChannel(CHANNEL_TAMPERED, ChannelKind.STATE); - } - - // e.g. Aqara Cube - if (sensorState.gesture != null) { - createChannel(CHANNEL_GESTURE, ChannelKind.STATE); - createChannel(CHANNEL_GESTUREEVENT, ChannelKind.TRIGGER); - } - ignoreConfigurationUpdate = false; - - // Initial data - updateChannels(sensorConfig); - updateChannels(sensorState, true); - - updateStatus(ThingStatus.ONLINE); - } - - private void createChannel(String channelId, ChannelKind kind) { - ThingHandlerCallback callback = getCallback(); - if (callback != null) { - ChannelUID channelUID = new ChannelUID(thing.getUID(), channelId); - ChannelTypeUID channelTypeUID; - switch (channelId) { - case CHANNEL_BATTERY_LEVEL: - channelTypeUID = new ChannelTypeUID("system:battery-level"); - break; - case CHANNEL_BATTERY_LOW: - channelTypeUID = new ChannelTypeUID("system:low-battery"); - break; - default: - channelTypeUID = new ChannelTypeUID(BINDING_ID, channelId); - break; - } - Channel channel = callback.createChannelBuilder(channelUID, channelTypeUID).withKind(kind).build(); - updateThing(editThing().withoutChannel(channelUID).withChannel(channel).build()); - } - } - - public void valueUpdated(ChannelUID channelUID, SensorConfig newConfig) { - Integer batteryLevel = newConfig.battery; + protected void valueUpdated(ChannelUID channelUID, SensorConfig newConfig) { + super.valueUpdated(channelUID, newConfig); Float temperature = newConfig.temperature; switch (channelUID.getId()) { - case CHANNEL_BATTERY_LEVEL: - if (batteryLevel != null) { - updateState(channelUID, new DecimalType(batteryLevel.longValue())); - } - break; - case CHANNEL_BATTERY_LOW: - if (batteryLevel != null) { - updateState(channelUID, OnOffType.from(batteryLevel <= 10)); - } - break; case CHANNEL_TEMPERATURE: if (temperature != null) { updateState(channelUID, new QuantityType<>(temperature / 100, CELSIUS)); @@ -277,7 +98,9 @@ public void valueUpdated(ChannelUID channelUID, SensorConfig newConfig) { } } - public void valueUpdated(String channelID, SensorState newState, boolean initializing) { + @Override + protected void valueUpdated(String channelID, SensorState newState, boolean initializing) { + super.valueUpdated(channelID, newState, initializing); switch (channelID) { case CHANNEL_LIGHT: Boolean dark = newState.dark; @@ -377,71 +200,49 @@ public void valueUpdated(String channelID, SensorState newState, boolean initial triggerChannel(channelID, String.valueOf(gesture)); } break; - case CHANNEL_BATTERY_LEVEL: - updateDecimalTypeChannel(channelID, newState.battery); - break; - case CHANNEL_LAST_UPDATED: - String lastUpdated = newState.lastupdated; - if (lastUpdated != null && !"none".equals(lastUpdated)) { - updateState(channelID, - new DateTimeType(ZonedDateTime.ofInstant( - LocalDateTime.parse(lastUpdated, DateTimeFormatter.ISO_LOCAL_DATE_TIME), - ZoneOffset.UTC, ZoneId.systemDefault()))); - } - break; } } @Override - public void messageReceived(String sensorID, DeconzBaseMessage message) { - if (message instanceof SensorMessage) { - SensorMessage sensorMessage = (SensorMessage) message; - SensorConfig sensorConfig = sensorMessage.config; - if (sensorConfig != null) { - this.sensorConfig = sensorConfig; - updateChannels(sensorConfig); - } - SensorState sensorState = sensorMessage.state; - if (sensorState != null) { - updateChannels(sensorState, false); - } + protected void createTypeSpecificChannels(SensorConfig sensorConfig, SensorState sensorState) { + // some Xiaomi sensors + if (sensorConfig.temperature != null) { + createChannel(CHANNEL_TEMPERATURE, ChannelKind.STATE); } - } - private void updateChannels(SensorConfig newConfig) { - thing.getChannels().stream().map(Channel::getUID) - .filter(channelUID -> CONFIG_CHANNELS.contains(channelUID.getId())) - .forEach((channelUID) -> valueUpdated(channelUID, newConfig)); - } + // ZHAPresence - e.g. IKEA TRÅDFRI motion sensor + if (sensorState.dark != null) { + createChannel(CHANNEL_DARK, ChannelKind.STATE); + } - private void updateChannels(SensorState newState, boolean initializing) { - logger.trace("{} received {}", thing.getUID(), newState); - sensorState = newState; - thing.getChannels().forEach(channel -> valueUpdated(channel.getUID().getId(), newState, initializing)); - } + // ZHAConsumption - e.g Bitron 902010/25 or Heiman SmartPlug + if (sensorState.power != null) { + createChannel(CHANNEL_POWER, ChannelKind.STATE); + } - private void updateSwitchChannel(String channelID, @Nullable Boolean value) { - if (value == null) { - return; + // ZHAPower - e.g. Heiman SmartPlug + if (sensorState.voltage != null) { + createChannel(CHANNEL_VOLTAGE, ChannelKind.STATE); + } + if (sensorState.current != null) { + createChannel(CHANNEL_CURRENT, ChannelKind.STATE); } - updateState(channelID, OnOffType.from(value)); - } - private void updateDecimalTypeChannel(String channelID, @Nullable Number value) { - if (value == null) { - return; + // IAS Zone sensor - e.g. Heiman HS1MS motion sensor + if (sensorState.tampered != null) { + createChannel(CHANNEL_TAMPERED, ChannelKind.STATE); + } + + // e.g. Aqara Cube + if (sensorState.gesture != null) { + createChannel(CHANNEL_GESTURE, ChannelKind.STATE); + createChannel(CHANNEL_GESTUREEVENT, ChannelKind.TRIGGER); } - updateState(channelID, new DecimalType(value.longValue())); - } - private void updateQuantityTypeChannel(String channelID, @Nullable Number value, Unit unit) { - updateQuantityTypeChannel(channelID, value, unit, 1.0); } - private void updateQuantityTypeChannel(String channelID, @Nullable Number value, Unit unit, double scaling) { - if (value == null) { - return; - } - updateState(channelID, new QuantityType<>(value.doubleValue() * scaling, unit)); + @Override + protected List getConfigChannels() { + return CONFIG_CHANNELS; } } diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/types/ThermostatMode.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/types/ThermostatMode.java new file mode 100644 index 0000000000000..9a87682d23027 --- /dev/null +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/types/ThermostatMode.java @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.deconz.internal.types; + +import java.util.Arrays; +import java.util.Map; +import java.util.stream.Collectors; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Thermostat mode as reported by the REST API for usage in {@link org.openhab.binding.deconz.internal.dto.SensorConfig} + * + * @author Lukas Agethen - Initial contribution + */ +@NonNullByDefault +public enum ThermostatMode { + AUTO("auto"), + HEAT("heat"), + OFF("off"), + UNKNOWN(""); + + private static final Map MAPPING = Arrays.stream(ThermostatMode.values()) + .collect(Collectors.toMap(v -> v.deconzValue, v -> v)); + private static final Logger LOGGER = LoggerFactory.getLogger(ThermostatMode.class); + + private String deconzValue; + + ThermostatMode(String deconzValue) { + this.deconzValue = deconzValue; + } + + public String getDeconzValue() { + return deconzValue; + } + + public static ThermostatMode fromString(String s) { + ThermostatMode thermostatMode = MAPPING.getOrDefault(s, UNKNOWN); + if (thermostatMode == UNKNOWN) { + LOGGER.debug("Unknown thermostat mode '{}' found. This should be reported.", s); + } + return thermostatMode; + } +} diff --git a/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/types/ThermostatModeGsonTypeAdapter.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/types/ThermostatModeGsonTypeAdapter.java new file mode 100644 index 0000000000000..2b727f2117cdc --- /dev/null +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/types/ThermostatModeGsonTypeAdapter.java @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.deconz.internal.types; + +import java.lang.reflect.Type; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonNull; +import com.google.gson.JsonParseException; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; + +/** + * Custom (de)serializer for {@link ThermostatMode} + * + * @author Lukas Agethen - Initial contribution + */ +@NonNullByDefault +public class ThermostatModeGsonTypeAdapter implements JsonDeserializer, JsonSerializer { + @Override + public ThermostatMode deserialize(@Nullable JsonElement json, @Nullable Type typeOfT, + @Nullable JsonDeserializationContext context) throws JsonParseException { + JsonElement jsonLocal = json; + if (jsonLocal != null) { + String s = jsonLocal.getAsString(); + return s == null ? ThermostatMode.UNKNOWN : ThermostatMode.fromString(s); + } + return ThermostatMode.UNKNOWN; + } + + @Override + public JsonElement serialize(ThermostatMode src, @Nullable Type typeOfSrc, + @Nullable JsonSerializationContext context) throws JsonParseException { + return src != ThermostatMode.UNKNOWN ? new JsonPrimitive(src.getDeconzValue()) : JsonNull.INSTANCE; + + } +} diff --git a/bundles/org.openhab.binding.deconz/src/main/resources/ESH-INF/thing/sensor-thing-types.xml b/bundles/org.openhab.binding.deconz/src/main/resources/ESH-INF/thing/sensor-thing-types.xml index e42ef5b0076c8..8c3ac5b38e3a7 100644 --- a/bundles/org.openhab.binding.deconz/src/main/resources/ESH-INF/thing/sensor-thing-types.xml +++ b/bundles/org.openhab.binding.deconz/src/main/resources/ESH-INF/thing/sensor-thing-types.xml @@ -475,5 +475,53 @@ + + + + + + A Thermostat sensor/actor + + + + + + + + + uid + + + + + Number:Temperature + + Target temperature + + + + String + + Current mode + + + + + + + + + + Number:Temperature + + Temperature offset + + + + Number:Dimensionless + + Current valve position + + diff --git a/bundles/org.openhab.binding.deconz/src/test/java/org/openhab/binding/deconz/DeconzTest.java b/bundles/org.openhab.binding.deconz/src/test/java/org/openhab/binding/deconz/DeconzTest.java index d339a3fc3c13b..5b965848ac014 100644 --- a/bundles/org.openhab.binding.deconz/src/test/java/org/openhab/binding/deconz/DeconzTest.java +++ b/bundles/org.openhab.binding.deconz/src/test/java/org/openhab/binding/deconz/DeconzTest.java @@ -34,6 +34,8 @@ import org.openhab.binding.deconz.internal.handler.DeconzBridgeHandler; import org.openhab.binding.deconz.internal.types.LightType; import org.openhab.binding.deconz.internal.types.LightTypeDeserializer; +import org.openhab.binding.deconz.internal.types.ThermostatMode; +import org.openhab.binding.deconz.internal.types.ThermostatModeGsonTypeAdapter; import com.google.gson.Gson; import com.google.gson.GsonBuilder; @@ -65,6 +67,7 @@ public void initialize() { GsonBuilder gsonBuilder = new GsonBuilder(); gsonBuilder.registerTypeAdapter(LightType.class, new LightTypeDeserializer()); + gsonBuilder.registerTypeAdapter(ThermostatMode.class, new ThermostatModeGsonTypeAdapter()); gson = gsonBuilder.create(); } @@ -85,6 +88,6 @@ public void discoveryTest() throws IOException { public static T getObjectFromJson(String filename, Class clazz, Gson gson) throws IOException { String json = IOUtils.toString(DeconzTest.class.getResourceAsStream(filename), StandardCharsets.UTF_8.name()); - return (T) gson.fromJson(json, clazz); + return gson.fromJson(json, clazz); } } diff --git a/bundles/org.openhab.binding.deconz/src/test/java/org/openhab/binding/deconz/LightsTest.java b/bundles/org.openhab.binding.deconz/src/test/java/org/openhab/binding/deconz/LightsTest.java index 1e15128bc1049..57657cc144bc4 100644 --- a/bundles/org.openhab.binding.deconz/src/test/java/org/openhab/binding/deconz/LightsTest.java +++ b/bundles/org.openhab.binding.deconz/src/test/java/org/openhab/binding/deconz/LightsTest.java @@ -36,6 +36,8 @@ import org.openhab.binding.deconz.internal.handler.LightThingHandler; import org.openhab.binding.deconz.internal.types.LightType; import org.openhab.binding.deconz.internal.types.LightTypeDeserializer; +import org.openhab.binding.deconz.internal.types.ThermostatMode; +import org.openhab.binding.deconz.internal.types.ThermostatModeGsonTypeAdapter; import com.google.gson.Gson; import com.google.gson.GsonBuilder; @@ -58,6 +60,7 @@ public void initialize() { GsonBuilder gsonBuilder = new GsonBuilder(); gsonBuilder.registerTypeAdapter(LightType.class, new LightTypeDeserializer()); + gsonBuilder.registerTypeAdapter(ThermostatMode.class, new ThermostatModeGsonTypeAdapter()); gson = gsonBuilder.create(); } diff --git a/bundles/org.openhab.binding.deconz/src/test/java/org/openhab/binding/deconz/SensorsTest.java b/bundles/org.openhab.binding.deconz/src/test/java/org/openhab/binding/deconz/SensorsTest.java index 66fcab59b0f32..eea0e526093b2 100644 --- a/bundles/org.openhab.binding.deconz/src/test/java/org/openhab/binding/deconz/SensorsTest.java +++ b/bundles/org.openhab.binding.deconz/src/test/java/org/openhab/binding/deconz/SensorsTest.java @@ -14,12 +14,16 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.MockitoAnnotations.initMocks; -import static org.openhab.binding.deconz.internal.BindingConstants.THING_TYPE_CARBONMONOXIDE_SENSOR; +import static org.openhab.binding.deconz.internal.BindingConstants.*; import java.io.IOException; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.smarthome.core.library.types.OnOffType; +import org.eclipse.smarthome.core.library.types.QuantityType; +import org.eclipse.smarthome.core.library.types.StringType; +import org.eclipse.smarthome.core.library.unit.SIUnits; +import org.eclipse.smarthome.core.library.unit.SmartHomeUnits; import org.eclipse.smarthome.core.thing.ChannelUID; import org.eclipse.smarthome.core.thing.Thing; import org.eclipse.smarthome.core.thing.ThingUID; @@ -32,9 +36,12 @@ import org.mockito.Mock; import org.mockito.Mockito; import org.openhab.binding.deconz.internal.dto.SensorMessage; +import org.openhab.binding.deconz.internal.handler.SensorThermostatThingHandler; import org.openhab.binding.deconz.internal.handler.SensorThingHandler; import org.openhab.binding.deconz.internal.types.LightType; import org.openhab.binding.deconz.internal.types.LightTypeDeserializer; +import org.openhab.binding.deconz.internal.types.ThermostatMode; +import org.openhab.binding.deconz.internal.types.ThermostatModeGsonTypeAdapter; import com.google.gson.Gson; import com.google.gson.GsonBuilder; @@ -43,6 +50,7 @@ * This class provides tests for deconz sensors * * @author Jan N. Klug - Initial contribution + * @author Lukas Agethen - Added Thermostat */ @NonNullByDefault public class SensorsTest { @@ -57,6 +65,7 @@ public void initialize() { GsonBuilder gsonBuilder = new GsonBuilder(); gsonBuilder.registerTypeAdapter(LightType.class, new LightTypeDeserializer()); + gsonBuilder.registerTypeAdapter(ThermostatMode.class, new ThermostatModeGsonTypeAdapter()); gson = gsonBuilder.create(); } @@ -75,4 +84,33 @@ public void carbonmonoxideSensorUpdateTest() throws IOException { sensorThingHandler.messageReceived("", sensorMessage); Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelUID), eq(OnOffType.ON)); } + + @Test + public void thermostatSensorUpdateTest() throws IOException { + SensorMessage sensorMessage = DeconzTest.getObjectFromJson("thermostat.json", SensorMessage.class, gson); + Assert.assertNotNull(sensorMessage); + + ThingUID thingUID = new ThingUID("deconz", "sensor"); + ChannelUID channelValveUID = new ChannelUID(thingUID, "valve"); + ChannelUID channelHeatSetPointUID = new ChannelUID(thingUID, "heatsetpoint"); + ChannelUID channelModeUID = new ChannelUID(thingUID, "mode"); + ChannelUID channelTemperatureUID = new ChannelUID(thingUID, "temperature"); + Thing sensor = ThingBuilder.create(THING_TYPE_THERMOSTAT, thingUID) + .withChannel(ChannelBuilder.create(channelValveUID, "Number").build()) + .withChannel(ChannelBuilder.create(channelHeatSetPointUID, "Number").build()) + .withChannel(ChannelBuilder.create(channelModeUID, "String").build()) + .withChannel(ChannelBuilder.create(channelTemperatureUID, "Number").build()).build(); + SensorThermostatThingHandler sensorThingHandler = new SensorThermostatThingHandler(sensor, gson); + sensorThingHandler.setCallback(thingHandlerCallback); + + sensorThingHandler.messageReceived("", sensorMessage); + Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelValveUID), + eq(new QuantityType<>(100.0, SmartHomeUnits.PERCENT))); + Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelHeatSetPointUID), + eq(new QuantityType<>(25, SIUnits.CELSIUS))); + Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelModeUID), + eq(new StringType(ThermostatMode.AUTO.name()))); + Mockito.verify(thingHandlerCallback).stateUpdated(eq(channelTemperatureUID), + eq(new QuantityType<>(16.5, SIUnits.CELSIUS))); + } } diff --git a/bundles/org.openhab.binding.deconz/src/test/resources/org/openhab/binding/deconz/thermostat.json b/bundles/org.openhab.binding.deconz/src/test/resources/org/openhab/binding/deconz/thermostat.json new file mode 100644 index 0000000000000..0d314fe6d7ef4 --- /dev/null +++ b/bundles/org.openhab.binding.deconz/src/test/resources/org/openhab/binding/deconz/thermostat.json @@ -0,0 +1,27 @@ +{ + "config": { + "battery": 85, + "displayflipped": null, + "heatsetpoint": 2500, + "locked": null, + "mode": "auto", + "offset": 0, + "on": true, + "reachable": true + }, + "ep": 1, + "etag": "717549a99371f3ea1a5f0b40f1537094", + "lastseen": "2020-05-31T20:24:55.819", + "manufacturername": "Eurotronic", + "modelid": "SPZB0001", + "name": "Test Thermostat", + "state": { + "lastupdated": "2020-05-31T20:24:55.819", + "on": true, + "temperature": 1650, + "valve": 255 + }, + "swversion": "20191014", + "type": "ZHAThermostat", + "uniqueid": "00:15:8d:00:01:ff:8a:00-01-0201" +} \ No newline at end of file