From 64c83c0da49807b3847503fbc4b0cf7cd87155b6 Mon Sep 17 00:00:00 2001 From: Lukas Agethen Date: Sun, 31 May 2020 23:29:00 +0200 Subject: [PATCH 1/7] [deconz] Add support for ZHAThermostat Adds support for Zigbee Thermostat based on Eurotronics Spirit Closes #6251 Signed-off-by: Lukas Agethen --- .../deconz/internal/BindingConstants.java | 6 + .../deconz/internal/DeconzHandlerFactory.java | 5 +- .../discovery/ThingDiscoveryService.java | 6 +- .../deconz/internal/dto/SensorConfig.java | 3 + .../deconz/internal/dto/SensorState.java | 2 + .../handler/SensorBaseThingHandler.java | 282 ++++++++++++++++++ .../handler/SensorThermostatThingHandler.java | 189 ++++++++++++ .../internal/handler/SensorThingHandler.java | 271 +++-------------- .../ESH-INF/thing/sensor-thing-types.xml | 48 +++ .../openhab/binding/deconz/SensorsTest.java | 36 ++- .../openhab/binding/deconz/thermostat.json | 27 ++ 11 files changed, 637 insertions(+), 238 deletions(-) create mode 100644 bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/SensorBaseThingHandler.java create mode 100644 bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/SensorThermostatThingHandler.java create mode 100644 bundles/org.openhab.binding.deconz/src/test/resources/org/openhab/binding/deconz/thermostat.json 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..c392f5698a463 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,6 +28,7 @@ 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; @@ -50,7 +51,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; @@ -84,6 +85,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..82cf9aa9a948e 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 @@ -30,4 +30,7 @@ public class SensorConfig { public boolean reachable = true; public @Nullable Integer battery; public @Nullable Float temperature; + public @Nullable Integer heatsetpoint; + public @Nullable String 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/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..7e91232e2617a --- /dev/null +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/SensorBaseThingHandler.java @@ -0,0 +1,282 @@ +/** + * 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()); + } + } + + public 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; + } + } + + public 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; + } + } + + @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..8b02800ea4786 --- /dev/null +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/SensorThermostatThingHandler.java @@ -0,0 +1,189 @@ +/** + * 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 java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.eclipse.jdt.annotation.NonNullByDefault; +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.thing.type.ChannelKind; +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.netutils.AsyncHttpClient; +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 + .unmodifiableSet(Stream.of(THING_TYPE_THERMOSTAT).collect(Collectors.toSet())); + + 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; + } + SensorConfig newConfig = new SensorConfig(); + String channelId = channelUID.getId(); + switch (channelId) { + case CHANNEL_HEATSETPOINT: + BigDecimal newTemperature; + if (command instanceof DecimalType) { + newTemperature = ((DecimalType) command).toBigDecimal(); + } else if (command instanceof QuantityType) { + newTemperature = ((QuantityType) command).toUnit(CELSIUS).toBigDecimal(); + } else { + return; + } + newConfig.heatsetpoint = newTemperature.scaleByPowerOfTen(2).intValue(); + break; + case CHANNEL_THERMOSTAT_MODE: + if (command instanceof StringType) { + newConfig.mode = ((StringType) command).toString(); + } 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 -> { + logger.trace("Result code={}, body={}", v.getResponseCode(), v.getBody()); + }).exceptionally(e -> { + logger.debug("Sending command {} to channel {} failed:", command, channelUID, e); + return null; + }); + } + + @Override + public void valueUpdated(ChannelUID channelUID, SensorConfig newConfig) { + super.valueUpdated(channelUID, newConfig); + String mode = newConfig.mode; + String channelID = channelUID.getId(); + switch (channelID) { + case CHANNEL_HEATSETPOINT: + updateQuantityTypeChannel(channelID, newConfig.heatsetpoint, CELSIUS, 1.0 / 100); + break; + case CHANNEL_TEMPERATURE_OFFSET: + updateDecimalTypeChannel(channelID, newConfig.offset); + break; + case CHANNEL_THERMOSTAT_MODE: + if (mode != null) { + updateState(channelUID, new StringType(mode)); + } + break; + } + } + + @Override + public 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) { + // some Xiaomi sensors + if (sensorConfig.temperature != null || sensorState.temperature != null) { + createChannel(CHANNEL_TEMPERATURE, ChannelKind.STATE); + } + + // (Eurotronics) Thermostat + if (sensorState.valve != null) { + createChannel(CHANNEL_VALVE_POSITION, ChannelKind.STATE); + } + + if (sensorConfig.heatsetpoint != null) { + createChannel(CHANNEL_HEATSETPOINT, ChannelKind.STATE); + } + + if (sensorConfig.mode != null) { + createChannel(CHANNEL_THERMOSTAT_MODE, ChannelKind.STATE); + } + + if (sensorConfig.offset != null) { + createChannel(CHANNEL_TEMPERATURE_OFFSET, ChannelKind.STATE); + } + + } + + @Override + protected List getConfigChannels() { + return CONFIG_CHANNELS; + } +} 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..b810e16992c42 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; + 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) { } } + @Override public 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/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..8e61248a380f6 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 offset + + + + Number:Dimensionless + + Current valve position + + 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..ac6f6c13c1e38 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,6 +36,7 @@ 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; @@ -43,6 +48,7 @@ * This class provides tests for deconz sensors * * @author Jan N. Klug - Initial contribution + * @author Lukas Agethen - Added Thermostat */ @NonNullByDefault public class SensorsTest { @@ -75,4 +81,32 @@ 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("auto"))); + 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 From 32b52ed57a442ef707ce1d659ac123692c2cdc7f Mon Sep 17 00:00:00 2001 From: Lukas Agethen Date: Sun, 31 May 2020 23:29:00 +0200 Subject: [PATCH 2/7] [deconz] Add support for ZHAThermostat Adds support for Zigbee Thermostat based on Eurotronics Spirit Closes #6251 Signed-off-by: Lukas Agethen --- bundles/org.openhab.binding.deconz/README.md | 11 +- .../deconz/internal/BindingConstants.java | 6 + .../deconz/internal/DeconzHandlerFactory.java | 5 +- .../discovery/ThingDiscoveryService.java | 6 +- .../deconz/internal/dto/SensorConfig.java | 3 + .../deconz/internal/dto/SensorState.java | 2 + .../handler/SensorBaseThingHandler.java | 282 ++++++++++++++++++ .../handler/SensorThermostatThingHandler.java | 189 ++++++++++++ .../internal/handler/SensorThingHandler.java | 271 +++-------------- .../ESH-INF/thing/sensor-thing-types.xml | 48 +++ .../openhab/binding/deconz/SensorsTest.java | 36 ++- .../openhab/binding/deconz/thermostat.json | 27 ++ 12 files changed, 646 insertions(+), 240 deletions(-) create mode 100644 bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/SensorBaseThingHandler.java create mode 100644 bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/SensorThermostatThingHandler.java create mode 100644 bundles/org.openhab.binding.deconz/src/test/resources/org/openhab/binding/deconz/thermostat.json 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..c392f5698a463 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,6 +28,7 @@ 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; @@ -50,7 +51,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; @@ -84,6 +85,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..82cf9aa9a948e 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 @@ -30,4 +30,7 @@ public class SensorConfig { public boolean reachable = true; public @Nullable Integer battery; public @Nullable Float temperature; + public @Nullable Integer heatsetpoint; + public @Nullable String 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/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..7e91232e2617a --- /dev/null +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/SensorBaseThingHandler.java @@ -0,0 +1,282 @@ +/** + * 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()); + } + } + + public 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; + } + } + + public 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; + } + } + + @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..8b02800ea4786 --- /dev/null +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/handler/SensorThermostatThingHandler.java @@ -0,0 +1,189 @@ +/** + * 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 java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.eclipse.jdt.annotation.NonNullByDefault; +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.thing.type.ChannelKind; +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.netutils.AsyncHttpClient; +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 + .unmodifiableSet(Stream.of(THING_TYPE_THERMOSTAT).collect(Collectors.toSet())); + + 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; + } + SensorConfig newConfig = new SensorConfig(); + String channelId = channelUID.getId(); + switch (channelId) { + case CHANNEL_HEATSETPOINT: + BigDecimal newTemperature; + if (command instanceof DecimalType) { + newTemperature = ((DecimalType) command).toBigDecimal(); + } else if (command instanceof QuantityType) { + newTemperature = ((QuantityType) command).toUnit(CELSIUS).toBigDecimal(); + } else { + return; + } + newConfig.heatsetpoint = newTemperature.scaleByPowerOfTen(2).intValue(); + break; + case CHANNEL_THERMOSTAT_MODE: + if (command instanceof StringType) { + newConfig.mode = ((StringType) command).toString(); + } 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 -> { + logger.trace("Result code={}, body={}", v.getResponseCode(), v.getBody()); + }).exceptionally(e -> { + logger.debug("Sending command {} to channel {} failed:", command, channelUID, e); + return null; + }); + } + + @Override + public void valueUpdated(ChannelUID channelUID, SensorConfig newConfig) { + super.valueUpdated(channelUID, newConfig); + String mode = newConfig.mode; + String channelID = channelUID.getId(); + switch (channelID) { + case CHANNEL_HEATSETPOINT: + updateQuantityTypeChannel(channelID, newConfig.heatsetpoint, CELSIUS, 1.0 / 100); + break; + case CHANNEL_TEMPERATURE_OFFSET: + updateDecimalTypeChannel(channelID, newConfig.offset); + break; + case CHANNEL_THERMOSTAT_MODE: + if (mode != null) { + updateState(channelUID, new StringType(mode)); + } + break; + } + } + + @Override + public 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) { + // some Xiaomi sensors + if (sensorConfig.temperature != null || sensorState.temperature != null) { + createChannel(CHANNEL_TEMPERATURE, ChannelKind.STATE); + } + + // (Eurotronics) Thermostat + if (sensorState.valve != null) { + createChannel(CHANNEL_VALVE_POSITION, ChannelKind.STATE); + } + + if (sensorConfig.heatsetpoint != null) { + createChannel(CHANNEL_HEATSETPOINT, ChannelKind.STATE); + } + + if (sensorConfig.mode != null) { + createChannel(CHANNEL_THERMOSTAT_MODE, ChannelKind.STATE); + } + + if (sensorConfig.offset != null) { + createChannel(CHANNEL_TEMPERATURE_OFFSET, ChannelKind.STATE); + } + + } + + @Override + protected List getConfigChannels() { + return CONFIG_CHANNELS; + } +} 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..b810e16992c42 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; + 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) { } } + @Override public 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/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..8e61248a380f6 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 offset + + + + Number:Dimensionless + + Current valve position + + 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..ac6f6c13c1e38 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,6 +36,7 @@ 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; @@ -43,6 +48,7 @@ * This class provides tests for deconz sensors * * @author Jan N. Klug - Initial contribution + * @author Lukas Agethen - Added Thermostat */ @NonNullByDefault public class SensorsTest { @@ -75,4 +81,32 @@ 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("auto"))); + 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 From 0ca41cbe246a332af7a2061c33576b9b38039793 Mon Sep 17 00:00:00 2001 From: Lukas Agethen Date: Tue, 2 Jun 2020 16:29:05 +0200 Subject: [PATCH 3/7] Refactored based on suggestions Signed-off-by: Lukas Agethen --- .../deconz/internal/DeconzHandlerFactory.java | 3 + .../deconz/internal/dto/SensorConfig.java | 6 +- .../deconz/internal/dto/ThermostatConfig.java | 29 +++++ .../handler/SensorBaseThingHandler.java | 21 +++- .../handler/SensorThermostatThingHandler.java | 112 ++++++++++++------ .../internal/handler/SensorThingHandler.java | 4 +- .../deconz/internal/types/ThermostatMode.java | 56 +++++++++ .../types/ThermostatModeGsonTypeAdapter.java | 43 +++++++ .../ESH-INF/thing/sensor-thing-types.xml | 10 +- .../openhab/binding/deconz/DeconzTest.java | 5 +- .../openhab/binding/deconz/LightsTest.java | 3 + .../openhab/binding/deconz/SensorsTest.java | 6 +- 12 files changed, 245 insertions(+), 53 deletions(-) create mode 100644 bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/dto/ThermostatConfig.java create mode 100644 bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/types/ThermostatMode.java create mode 100644 bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/types/ThermostatModeGsonTypeAdapter.java 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 c392f5698a463..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 @@ -33,6 +33,8 @@ 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; @@ -66,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(); } 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 82cf9aa9a948e..05d84e6581df0 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 @@ -23,14 +23,12 @@ * https://dresden-elektronik.github.io/deconz-rest-doc/sensors/. * * @author David Graeff - Initial contribution + * @author Lukas Agethen - Add Thermostat parameters */ @NonNullByDefault -public class SensorConfig { +public class SensorConfig extends ThermostatConfig { public boolean on = true; public boolean reachable = true; public @Nullable Integer battery; public @Nullable Float temperature; - public @Nullable Integer heatsetpoint; - public @Nullable String mode; - public @Nullable Integer offset; } 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 index 7e91232e2617a..3a86375575874 100644 --- 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 @@ -196,7 +196,13 @@ protected void createChannel(String channelId, ChannelKind kind) { } } - public void valueUpdated(ChannelUID channelUID, SensorConfig newConfig) { + /** + * 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: @@ -209,10 +215,19 @@ public void valueUpdated(ChannelUID channelUID, SensorConfig newConfig) { updateState(channelUID, OnOffType.from(batteryLevel <= 10)); } break; + default: + // other cases covered by sub-class } } - public void valueUpdated(String channelID, SensorState newState, boolean initializing) { + /** + * 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; @@ -223,6 +238,8 @@ public void valueUpdated(String channelID, SensorState newState, boolean initial ZoneOffset.UTC, ZoneId.systemDefault()))); } break; + default: + // other cases covered by sub-class } } 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 index 8b02800ea4786..484f63bd29fbc 100644 --- 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 @@ -26,18 +26,20 @@ import java.util.stream.Stream; 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.thing.type.ChannelKind; 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; @@ -67,7 +69,6 @@ public class SensorThermostatThingHandler extends SensorBaseThingHandler { private final Logger logger = LoggerFactory.getLogger(SensorThermostatThingHandler.class); - public SensorThermostatThingHandler(Thing thing, Gson gson) { super(thing, gson); } @@ -79,23 +80,40 @@ public void handleCommand(ChannelUID channelUID, Command command) { valueUpdated(channelUID.getId(), sensorState, false); return; } - SensorConfig newConfig = new SensorConfig(); + ThermostatConfig newConfig = new ThermostatConfig(); String channelId = channelUID.getId(); switch (channelId) { case CHANNEL_HEATSETPOINT: - BigDecimal newTemperature; - if (command instanceof DecimalType) { - newTemperature = ((DecimalType) command).toBigDecimal(); - } else if (command instanceof QuantityType) { - newTemperature = ((QuantityType) command).toUnit(CELSIUS).toBigDecimal(); - } else { + 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.heatsetpoint = newTemperature.scaleByPowerOfTen(2).intValue(); + newConfig.offset = newOffset; break; case CHANNEL_THERMOSTAT_MODE: if (command instanceof StringType) { - newConfig.mode = ((StringType) command).toString(); + 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; } @@ -116,7 +134,12 @@ public void handleCommand(ChannelUID channelUID, Command command) { String json = gson.toJson(newConfig); logger.trace("Sending {} to sensor {} via {}", json, config.id, url); asyncHttpClient.put(url, json, bridgeConfig.timeout).thenAccept(v -> { - logger.trace("Result code={}, body={}", v.getResponseCode(), v.getBody()); + 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; @@ -124,16 +147,16 @@ public void handleCommand(ChannelUID channelUID, Command command) { } @Override - public void valueUpdated(ChannelUID channelUID, SensorConfig newConfig) { + protected void valueUpdated(ChannelUID channelUID, SensorConfig newConfig) { super.valueUpdated(channelUID, newConfig); - String mode = newConfig.mode; + 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: - updateDecimalTypeChannel(channelID, newConfig.offset); + updateQuantityTypeChannel(channelID, newConfig.offset, CELSIUS, 1.0 / 100); break; case CHANNEL_THERMOSTAT_MODE: if (mode != null) { @@ -144,7 +167,7 @@ public void valueUpdated(ChannelUID channelUID, SensorConfig newConfig) { } @Override - public void valueUpdated(String channelID, SensorState newState, boolean initializing) { + protected void valueUpdated(String channelID, SensorState newState, boolean initializing) { super.valueUpdated(channelID, newState, initializing); switch (channelID) { case CHANNEL_TEMPERATURE: @@ -158,32 +181,45 @@ public void valueUpdated(String channelID, SensorState newState, boolean initial @Override protected void createTypeSpecificChannels(SensorConfig sensorConfig, SensorState sensorState) { - // some Xiaomi sensors - if (sensorConfig.temperature != null || sensorState.temperature != null) { - createChannel(CHANNEL_TEMPERATURE, ChannelKind.STATE); - } - - // (Eurotronics) Thermostat - if (sensorState.valve != null) { - createChannel(CHANNEL_VALVE_POSITION, ChannelKind.STATE); - } - - if (sensorConfig.heatsetpoint != null) { - createChannel(CHANNEL_HEATSETPOINT, ChannelKind.STATE); - } - - if (sensorConfig.mode != null) { - createChannel(CHANNEL_THERMOSTAT_MODE, ChannelKind.STATE); - } - - if (sensorConfig.offset != null) { - createChannel(CHANNEL_TEMPERATURE_OFFSET, ChannelKind.STATE); - } - + /* + * // some Xiaomi sensors + * if (sensorConfig.temperature != null || sensorState.temperature != null) { + * createChannel(CHANNEL_TEMPERATURE, ChannelKind.STATE); + * } + * + * // (Eurotronics) Thermostat + * if (sensorState.valve != null) { + * createChannel(CHANNEL_VALVE_POSITION, ChannelKind.STATE); + * } + * + * if (sensorConfig.heatsetpoint != null) { + * createChannel(CHANNEL_HEATSETPOINT, ChannelKind.STATE); + * } + * + * if (sensorConfig.mode != null) { + * createChannel(CHANNEL_THERMOSTAT_MODE, ChannelKind.STATE); + * } + * + * if (sensorConfig.offset != null) { + * createChannel(CHANNEL_TEMPERATURE_OFFSET, ChannelKind.STATE); + * } + */ } @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 b810e16992c42..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 @@ -85,7 +85,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { } @Override - public void valueUpdated(ChannelUID channelUID, SensorConfig newConfig) { + protected void valueUpdated(ChannelUID channelUID, SensorConfig newConfig) { super.valueUpdated(channelUID, newConfig); Float temperature = newConfig.temperature; @@ -99,7 +99,7 @@ public void valueUpdated(ChannelUID channelUID, SensorConfig newConfig) { } @Override - public void valueUpdated(String channelID, SensorState newState, boolean initializing) { + protected void valueUpdated(String channelID, SensorState newState, boolean initializing) { super.valueUpdated(channelID, newState, initializing); switch (channelID) { case CHANNEL_LIGHT: 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..1c30e1c79963a --- /dev/null +++ b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/types/ThermostatModeGsonTypeAdapter.java @@ -0,0 +1,43 @@ +/** + * 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 com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +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 + */ +public class ThermostatModeGsonTypeAdapter implements JsonDeserializer, JsonSerializer { + @Override + public ThermostatMode deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) + throws JsonParseException { + String s = json.getAsString(); + return s == null ? ThermostatMode.UNKNOWN : ThermostatMode.fromString(s); + } + + @Override + public JsonElement serialize(ThermostatMode src, Type typeOfSrc, JsonSerializationContext context) + throws JsonParseException { + return new JsonPrimitive(src != ThermostatMode.UNKNOWN ? src.getDeconzValue() : null); + } +} 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 8e61248a380f6..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 @@ -505,17 +505,17 @@ Current mode - - - + + + - Number + Number:Temperature Temperature offset - + Number:Dimensionless 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 ac6f6c13c1e38..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 @@ -40,6 +40,8 @@ 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; @@ -63,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(); } @@ -105,7 +108,8 @@ public void thermostatSensorUpdateTest() throws IOException { 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("auto"))); + 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))); } From 44ebd24cd1eb516a506e76c6ac3f3eaad59a7e50 Mon Sep 17 00:00:00 2001 From: Lukas Agethen Date: Tue, 2 Jun 2020 16:59:39 +0200 Subject: [PATCH 4/7] Fixed logging if update is not confirmed by "success" object Signed-off-by: Lukas Agethen --- .../deconz/internal/handler/SensorThermostatThingHandler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 484f63bd29fbc..036dbb2bfde34 100644 --- 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 @@ -137,7 +137,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { 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); + logger.debug("Sending command {} to channel {} failed: {}", command, channelUID, bodyContent); } }).exceptionally(e -> { From c852d95311e3ddb133070e5b8c37b378975d7a81 Mon Sep 17 00:00:00 2001 From: Lukas Agethen Date: Tue, 2 Jun 2020 19:58:06 +0200 Subject: [PATCH 5/7] Clean-up Signed-off-by: Lukas Agethen --- .../handler/SensorThermostatThingHandler.java | 28 +------------------ 1 file changed, 1 insertion(+), 27 deletions(-) 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 index 036dbb2bfde34..9e57d485a6093 100644 --- 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 @@ -22,8 +22,6 @@ import java.util.Collections; import java.util.List; import java.util.Set; -import java.util.stream.Collectors; -import java.util.stream.Stream; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -61,8 +59,7 @@ */ @NonNullByDefault public class SensorThermostatThingHandler extends SensorBaseThingHandler { - public static final Set SUPPORTED_THING_TYPES = Collections - .unmodifiableSet(Stream.of(THING_TYPE_THERMOSTAT).collect(Collectors.toSet())); + 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); @@ -181,29 +178,6 @@ protected void valueUpdated(String channelID, SensorState newState, boolean init @Override protected void createTypeSpecificChannels(SensorConfig sensorConfig, SensorState sensorState) { - /* - * // some Xiaomi sensors - * if (sensorConfig.temperature != null || sensorState.temperature != null) { - * createChannel(CHANNEL_TEMPERATURE, ChannelKind.STATE); - * } - * - * // (Eurotronics) Thermostat - * if (sensorState.valve != null) { - * createChannel(CHANNEL_VALVE_POSITION, ChannelKind.STATE); - * } - * - * if (sensorConfig.heatsetpoint != null) { - * createChannel(CHANNEL_HEATSETPOINT, ChannelKind.STATE); - * } - * - * if (sensorConfig.mode != null) { - * createChannel(CHANNEL_THERMOSTAT_MODE, ChannelKind.STATE); - * } - * - * if (sensorConfig.offset != null) { - * createChannel(CHANNEL_TEMPERATURE_OFFSET, ChannelKind.STATE); - * } - */ } @Override From 88dd1579896aadd58c64ef9f5557116b4361a2b6 Mon Sep 17 00:00:00 2001 From: Lukas Agethen Date: Fri, 5 Jun 2020 18:08:09 +0200 Subject: [PATCH 6/7] Minor refactoring Signed-off-by: Lukas Agethen --- .../deconz/internal/dto/SensorConfig.java | 6 +++++- .../types/ThermostatModeGsonTypeAdapter.java | 20 +++++++++++++------ 2 files changed, 19 insertions(+), 7 deletions(-) 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 05d84e6581df0..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. @@ -26,9 +27,12 @@ * @author Lukas Agethen - Add Thermostat parameters */ @NonNullByDefault -public class SensorConfig extends ThermostatConfig { +public class SensorConfig { public boolean on = true; 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/types/ThermostatModeGsonTypeAdapter.java b/bundles/org.openhab.binding.deconz/src/main/java/org/openhab/binding/deconz/internal/types/ThermostatModeGsonTypeAdapter.java index 1c30e1c79963a..ffe8a08ef6282 100644 --- 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 @@ -14,6 +14,9 @@ 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; @@ -27,17 +30,22 @@ * * @author Lukas Agethen - Initial contribution */ +@NonNullByDefault public class ThermostatModeGsonTypeAdapter implements JsonDeserializer, JsonSerializer { @Override - public ThermostatMode deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) - throws JsonParseException { - String s = json.getAsString(); - return s == null ? ThermostatMode.UNKNOWN : ThermostatMode.fromString(s); + 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, Type typeOfSrc, JsonSerializationContext context) - throws JsonParseException { + public JsonElement serialize(ThermostatMode src, @Nullable Type typeOfSrc, + @Nullable JsonSerializationContext context) throws JsonParseException { return new JsonPrimitive(src != ThermostatMode.UNKNOWN ? src.getDeconzValue() : null); } } From 3082f0a09fd639a63b051306eecc787f4bc2cc88 Mon Sep 17 00:00:00 2001 From: Lukas Agethen Date: Sat, 6 Jun 2020 13:30:24 +0200 Subject: [PATCH 7/7] Fix Thermostat mode JSON serialization Signed-off-by: Lukas Agethen --- .../deconz/internal/types/ThermostatModeGsonTypeAdapter.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 index ffe8a08ef6282..2b727f2117cdc 100644 --- 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 @@ -20,6 +20,7 @@ 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; @@ -46,6 +47,7 @@ public ThermostatMode deserialize(@Nullable JsonElement json, @Nullable Type typ @Override public JsonElement serialize(ThermostatMode src, @Nullable Type typeOfSrc, @Nullable JsonSerializationContext context) throws JsonParseException { - return new JsonPrimitive(src != ThermostatMode.UNKNOWN ? src.getDeconzValue() : null); + return src != ThermostatMode.UNKNOWN ? new JsonPrimitive(src.getDeconzValue()) : JsonNull.INSTANCE; + } }