2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.hue.internal.handler;
15 import static org.openhab.binding.hue.internal.HueBindingConstants.*;
16 import static org.openhab.binding.hue.internal.dto.FullSensor.*;
17 import static org.openhab.core.thing.Thing.*;
19 import java.time.LocalDateTime;
20 import java.time.ZoneId;
21 import java.time.ZoneOffset;
22 import java.time.ZonedDateTime;
23 import java.time.format.DateTimeFormatter;
24 import java.time.format.DateTimeParseException;
26 import java.util.Objects;
28 import org.eclipse.jdt.annotation.NonNullByDefault;
29 import org.eclipse.jdt.annotation.Nullable;
30 import org.openhab.binding.hue.internal.dto.FullSensor;
31 import org.openhab.binding.hue.internal.dto.SensorConfigUpdate;
32 import org.openhab.binding.hue.internal.dto.StateUpdate;
33 import org.openhab.core.config.core.Configuration;
34 import org.openhab.core.library.types.DateTimeType;
35 import org.openhab.core.library.types.DecimalType;
36 import org.openhab.core.library.types.OnOffType;
37 import org.openhab.core.thing.Bridge;
38 import org.openhab.core.thing.ChannelUID;
39 import org.openhab.core.thing.Thing;
40 import org.openhab.core.thing.ThingStatus;
41 import org.openhab.core.thing.ThingStatusDetail;
42 import org.openhab.core.thing.ThingStatusInfo;
43 import org.openhab.core.thing.binding.BaseThingHandler;
44 import org.openhab.core.thing.binding.ThingHandler;
45 import org.openhab.core.types.Command;
46 import org.slf4j.Logger;
47 import org.slf4j.LoggerFactory;
50 * Abstract Sensor Handler
52 * @author Samuel Leisering - Initial contribution
53 * @author Christoph Weitkamp - Initial contribution
56 public abstract class HueSensorHandler extends BaseThingHandler implements SensorStatusListener {
58 private final Logger logger = LoggerFactory.getLogger(HueSensorHandler.class);
60 private @NonNullByDefault({}) String sensorId;
62 private boolean configInitializedSuccessfully;
63 private boolean propertiesInitializedSuccessfully;
65 private @Nullable HueClient hueClient;
67 protected @Nullable FullSensor lastFullSensor;
69 public HueSensorHandler(Thing thing) {
74 public void initialize() {
75 logger.debug("Initializing Hue sensor handler.");
76 Bridge bridge = getBridge();
77 initializeThing((bridge == null) ? null : bridge.getStatus());
81 public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
82 logger.debug("bridgeStatusChanged {}", bridgeStatusInfo);
83 initializeThing(bridgeStatusInfo.getStatus());
86 private void initializeThing(@Nullable ThingStatus bridgeStatus) {
87 logger.debug("initializeThing thing {} bridge status {}", getThing().getUID(), bridgeStatus);
88 final String configSensorId = (String) getConfig().get(SENSOR_ID);
89 if (configSensorId != null) {
90 sensorId = configSensorId;
91 // note: this call implicitly registers our handler as a listener on the bridge
92 HueClient bridgeHandler = getHueClient();
93 if (bridgeHandler != null) {
94 if (bridgeStatus == ThingStatus.ONLINE) {
95 initializeProperties(bridgeHandler.getSensorById(sensorId));
96 updateStatus(ThingStatus.ONLINE);
98 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
101 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
104 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
105 "@text/offline.conf-error-no-sensor-id");
109 private synchronized void initializeProperties(@Nullable FullSensor fullSensor) {
110 if (!propertiesInitializedSuccessfully && fullSensor != null) {
111 Map<String, String> properties = editProperties();
112 String softwareVersion = fullSensor.getSoftwareVersion();
113 if (softwareVersion != null) {
114 properties.put(PROPERTY_FIRMWARE_VERSION, softwareVersion);
116 String modelId = fullSensor.getNormalizedModelID();
117 if (modelId != null) {
118 properties.put(PROPERTY_MODEL_ID, modelId);
120 properties.put(PROPERTY_VENDOR, fullSensor.getManufacturerName());
121 properties.put(PRODUCT_NAME, fullSensor.getProductName());
122 String uniqueID = fullSensor.getUniqueID();
123 if (uniqueID != null) {
124 properties.put(UNIQUE_ID, uniqueID);
126 updateProperties(properties);
127 propertiesInitializedSuccessfully = true;
132 public void dispose() {
133 logger.debug("Hue sensor handler disposes. Unregistering listener.");
134 if (sensorId != null) {
135 HueClient bridgeHandler = getHueClient();
136 if (bridgeHandler != null) {
137 bridgeHandler.unregisterSensorStatusListener(this);
144 protected synchronized @Nullable HueClient getHueClient() {
145 if (hueClient == null) {
146 Bridge bridge = getBridge();
147 if (bridge == null) {
150 ThingHandler handler = bridge.getHandler();
151 if (handler instanceof HueBridgeHandler) {
152 HueClient bridgeHandler = (HueClient) handler;
153 hueClient = bridgeHandler;
154 bridgeHandler.registerSensorStatusListener(this);
163 public void handleCommand(ChannelUID channelUID, Command command) {
164 handleCommand(channelUID.getId(), command);
167 protected void handleCommand(String channel, Command command) {
168 HueClient bridgeHandler = getHueClient();
169 if (bridgeHandler == null) {
170 logger.warn("Hue Bridge handler not found. Cannot handle command without bridge.");
174 final FullSensor sensor = lastFullSensor;
175 if (sensor == null) {
176 logger.debug("Hue sensor not known on bridge. Cannot handle command.");
177 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
178 "@text/offline.conf-error-wrong-sensor-id");
182 StateUpdate sensorState = new StateUpdate();
185 sensorState = sensorState.setStatus(((DecimalType) command).intValue());
188 sensorState = sensorState.setFlag(OnOffType.ON.equals(command));
192 if (sensorState != null) {
193 bridgeHandler.updateSensorState(sensor, sensorState);
195 logger.warn("Command sent to an unknown channel id: {}:{}", getThing().getUID(), channel);
200 public void handleConfigurationUpdate(Map<String, Object> configurationParameters) {
201 SensorConfigUpdate configUpdate = doConfigurationUpdate(configurationParameters);
202 if (configurationParameters.containsKey(CONFIG_ON)) {
203 configUpdate.setOn(Boolean.TRUE.equals(configurationParameters.get(CONFIG_ON)));
206 if (!configUpdate.isEmpty()) {
207 HueClient hueBridge = getHueClient();
208 if (hueBridge == null) {
209 logger.warn("Hue Bridge handler not found. Cannot handle configuration update without bridge.");
213 final FullSensor sensor = lastFullSensor;
214 if (sensor == null) {
215 logger.debug("Hue sensor not known on bridge. Cannot handle configuration update.");
216 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
217 "@text/offline.conf-error-wrong-sensor-id");
221 hueBridge.updateSensorConfig(sensor, configUpdate);
224 super.handleConfigurationUpdate(configurationParameters);
228 public boolean onSensorStateChanged(FullSensor sensor) {
229 logger.trace("onSensorStateChanged() was called");
231 final FullSensor lastSensor = lastFullSensor;
232 if (lastSensor == null || !Objects.equals(lastSensor.getState(), sensor.getState())) {
233 lastFullSensor = sensor;
238 logger.trace("New state for sensor {}", sensorId);
240 initializeProperties(sensor);
242 if (Boolean.TRUE.equals(sensor.getConfig().get(CONFIG_REACHABLE))) {
243 updateStatus(ThingStatus.ONLINE);
245 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "@text/offline.sensor-not-reachable");
248 // update generic sensor config
249 final Configuration config = !configInitializedSuccessfully ? editConfiguration() : getConfig();
250 if (sensor.getConfig().containsKey(CONFIG_ON)) {
251 config.put(CONFIG_ON, sensor.getConfig().get(CONFIG_ON));
254 // update specific sensor config
255 doSensorStateChanged(sensor, config);
257 Object lastUpdated = sensor.getState().get(STATE_LAST_UPDATED);
258 if (lastUpdated != null) {
260 updateState(CHANNEL_LAST_UPDATED,
261 new DateTimeType(ZonedDateTime.ofInstant(
262 LocalDateTime.parse(String.valueOf(lastUpdated), DateTimeFormatter.ISO_LOCAL_DATE_TIME),
263 ZoneOffset.UTC, ZoneId.systemDefault())));
264 } catch (DateTimeParseException e) {
269 Object status = sensor.getState().get(STATE_STATUS);
270 if (status != null) {
272 DecimalType value = new DecimalType(String.valueOf(status));
273 updateState(STATE_STATUS, value);
274 } catch (DateTimeParseException e) {
278 Object flag = sensor.getState().get(STATE_FLAG);
281 boolean value = Boolean.parseBoolean(String.valueOf(flag));
282 updateState(CHANNEL_FLAG, value ? OnOffType.ON : OnOffType.OFF);
283 } catch (DateTimeParseException e) {
288 Object battery = sensor.getConfig().get(CONFIG_BATTERY);
289 if (battery != null) {
290 DecimalType batteryLevel = DecimalType.valueOf(String.valueOf(battery));
291 updateState(CHANNEL_BATTERY_LEVEL, batteryLevel);
292 updateState(CHANNEL_BATTERY_LOW, batteryLevel.intValue() <= 10 ? OnOffType.ON : OnOffType.OFF);
295 if (!configInitializedSuccessfully) {
296 updateConfiguration(config);
297 configInitializedSuccessfully = true;
304 public void channelLinked(ChannelUID channelUID) {
305 final FullSensor sensor = lastFullSensor;
306 if (sensor != null) {
307 onSensorStateChanged(sensor);
312 * Handles the sensors configuration change.
314 * @param configurationParameters
317 protected abstract SensorConfigUpdate doConfigurationUpdate(Map<String, Object> configurationParameters);
320 * Handles the sensor change. Implementation should also update sensor-specific configuration that changed since the
323 * @param bridge the bridge
324 * @param sensor the sensor
325 * @param config the configuration in which to update the config states of the sensor
327 protected abstract void doSensorStateChanged(FullSensor sensor, Configuration config);
330 public void onSensorRemoved() {
331 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "@text/offline.sensor-not-reachable");
335 public void onSensorGone() {
336 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.GONE, "@text/offline.conf-error-wrong-sensor-id");
340 public void onSensorAdded(FullSensor sensor) {
341 onSensorStateChanged(sensor);
345 public String getSensorId() {