[deconz] fix lastSeenPolling and compile warnings (#8918)
Signed-off-by: Jan N. Klug <jan.n.klug@rub.de>
This commit is contained in:
parent
aaa464a255
commit
1dccf67909
|
@ -81,7 +81,7 @@ Auto-discovered things do not need to be configured.
|
||||||
All sensor-things have an additional `lastSeenPolling` parameter.
|
All sensor-things have an additional `lastSeenPolling` parameter.
|
||||||
Due to limitations in the API of deCONZ, the `lastSeen` channel (available some sensors) is only available when using polling.
|
Due to limitations in the API of deCONZ, the `lastSeen` channel (available some sensors) is only available when using polling.
|
||||||
Allowed values are all positive integers, the unit is minutes.
|
Allowed values are all positive integers, the unit is minutes.
|
||||||
The default-value is `0`, which means "no polling at all".
|
The default-value is `1440`, which means "once a day".
|
||||||
|
|
||||||
`dimmablelight`, `extendedcolorlight`, `colorlight` and `colortemperaturelight` have an additional optional parameter `transitiontime`.
|
`dimmablelight`, `extendedcolorlight`, `colorlight` and `colortemperaturelight` have an additional optional parameter `transitiontime`.
|
||||||
The transition time is the time to move between two states and is configured in seconds.
|
The transition time is the time to move between two states and is configured in seconds.
|
||||||
|
|
|
@ -163,11 +163,11 @@ public class ThingDiscoveryService extends AbstractDiscoveryService implements D
|
||||||
properties.put(Thing.PROPERTY_VENDOR, light.manufacturername);
|
properties.put(Thing.PROPERTY_VENDOR, light.manufacturername);
|
||||||
properties.put(Thing.PROPERTY_MODEL_ID, light.modelid);
|
properties.put(Thing.PROPERTY_MODEL_ID, light.modelid);
|
||||||
|
|
||||||
if (light.ctmax != null && light.ctmin != null) {
|
Integer ctmax = light.ctmax;
|
||||||
properties.put(PROPERTY_CT_MAX,
|
Integer ctmin = light.ctmin;
|
||||||
Integer.toString(Util.constrainToRange(light.ctmax, ZCL_CT_MIN, ZCL_CT_MAX)));
|
if (ctmax != null && ctmin != null) {
|
||||||
properties.put(PROPERTY_CT_MIN,
|
properties.put(PROPERTY_CT_MAX, Integer.toString(Util.constrainToRange(ctmax, ZCL_CT_MIN, ZCL_CT_MAX)));
|
||||||
Integer.toString(Util.constrainToRange(light.ctmin, ZCL_CT_MIN, ZCL_CT_MAX)));
|
properties.put(PROPERTY_CT_MIN, Integer.toString(Util.constrainToRange(ctmin, ZCL_CT_MIN, ZCL_CT_MAX)));
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (lightType) {
|
switch (lightType) {
|
||||||
|
|
|
@ -19,6 +19,7 @@ import java.util.concurrent.CompletionException;
|
||||||
import java.util.concurrent.ScheduledFuture;
|
import java.util.concurrent.ScheduledFuture;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.concurrent.TimeoutException;
|
import java.util.concurrent.TimeoutException;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
import org.eclipse.jdt.annotation.Nullable;
|
import org.eclipse.jdt.annotation.Nullable;
|
||||||
|
@ -119,7 +120,7 @@ public abstract class DeconzBaseThingHandler<T extends DeconzBaseMessage> extend
|
||||||
registerListener();
|
registerListener();
|
||||||
|
|
||||||
// get initial values
|
// get initial values
|
||||||
requestState();
|
requestState(this::processStateResponse);
|
||||||
} else {
|
} else {
|
||||||
// if the bridge is not ONLINE, we assume communication is not possible, so we unregister the listener and
|
// if the bridge is not ONLINE, we assume communication is not possible, so we unregister the listener and
|
||||||
// set the thing status to OFFLINE
|
// set the thing status to OFFLINE
|
||||||
|
@ -131,7 +132,7 @@ public abstract class DeconzBaseThingHandler<T extends DeconzBaseMessage> extend
|
||||||
protected abstract @Nullable T parseStateResponse(AsyncHttpClient.Result r);
|
protected abstract @Nullable T parseStateResponse(AsyncHttpClient.Result r);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* processes a newly received state response
|
* processes a newly received (initial) state response
|
||||||
*
|
*
|
||||||
* MUST set the thing status!
|
* MUST set the thing status!
|
||||||
*
|
*
|
||||||
|
@ -142,7 +143,7 @@ public abstract class DeconzBaseThingHandler<T extends DeconzBaseMessage> extend
|
||||||
/**
|
/**
|
||||||
* Perform a request to the REST API for retrieving the full light state with all data and configuration.
|
* Perform a request to the REST API for retrieving the full light state with all data and configuration.
|
||||||
*/
|
*/
|
||||||
protected void requestState() {
|
protected void requestState(Consumer<@Nullable T> processor) {
|
||||||
AsyncHttpClient asyncHttpClient = http;
|
AsyncHttpClient asyncHttpClient = http;
|
||||||
if (asyncHttpClient == null) {
|
if (asyncHttpClient == null) {
|
||||||
return;
|
return;
|
||||||
|
@ -162,10 +163,11 @@ public abstract class DeconzBaseThingHandler<T extends DeconzBaseMessage> extend
|
||||||
}
|
}
|
||||||
|
|
||||||
stopInitializationJob();
|
stopInitializationJob();
|
||||||
initializationJob = scheduler.schedule((Runnable) this::requestState, 10, TimeUnit.SECONDS);
|
initializationJob = scheduler.schedule(() -> requestState(this::processStateResponse), 10,
|
||||||
|
TimeUnit.SECONDS);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}).thenAccept(this::processStateResponse);
|
}).thenAccept(processor);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -91,8 +91,9 @@ public class GroupThingHandler extends DeconzBaseThingHandler<GroupMessage> {
|
||||||
case CHANNEL_COLOR:
|
case CHANNEL_COLOR:
|
||||||
if (command instanceof HSBType) {
|
if (command instanceof HSBType) {
|
||||||
HSBType hsbCommand = (HSBType) command;
|
HSBType hsbCommand = (HSBType) command;
|
||||||
newGroupAction.bri = Util.fromPercentType(hsbCommand.getBrightness());
|
Integer bri = Util.fromPercentType(hsbCommand.getBrightness());
|
||||||
if (newGroupAction.bri > 0) {
|
newGroupAction.bri = bri;
|
||||||
|
if (bri > 0) {
|
||||||
newGroupAction.hue = (int) (hsbCommand.getHue().doubleValue() * HUE_FACTOR);
|
newGroupAction.hue = (int) (hsbCommand.getHue().doubleValue() * HUE_FACTOR);
|
||||||
newGroupAction.sat = Util.fromPercentType(hsbCommand.getSaturation());
|
newGroupAction.sat = Util.fromPercentType(hsbCommand.getSaturation());
|
||||||
}
|
}
|
||||||
|
@ -118,7 +119,8 @@ public class GroupThingHandler extends DeconzBaseThingHandler<GroupMessage> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newGroupAction.bri != null && newGroupAction.bri > 0) {
|
Integer bri = newGroupAction.bri;
|
||||||
|
if (bri != null && bri > 0) {
|
||||||
newGroupAction.on = true;
|
newGroupAction.on = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -228,16 +228,17 @@ public class LightThingHandler extends DeconzBaseThingHandler<LightMessage> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newLightState.on != null && !newLightState.on) {
|
Boolean newOn = newLightState.on;
|
||||||
|
if (newOn != null && !newOn) {
|
||||||
// if light shall be off, no other commands are allowed, so reset the new light state
|
// if light shall be off, no other commands are allowed, so reset the new light state
|
||||||
newLightState.clear();
|
newLightState.clear();
|
||||||
newLightState.on = false;
|
newLightState.on = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
sendCommand(newLightState, command, channelUID, () -> {
|
sendCommand(newLightState, command, channelUID, () -> {
|
||||||
|
Integer transitionTime = newLightState.transitiontime;
|
||||||
lastCommandExpireTimestamp = System.currentTimeMillis()
|
lastCommandExpireTimestamp = System.currentTimeMillis()
|
||||||
+ (newLightState.transitiontime != null ? newLightState.transitiontime
|
+ (transitionTime != null ? transitionTime : DEFAULT_COMMAND_EXPIRY_TIME);
|
||||||
: DEFAULT_COMMAND_EXPIRY_TIME);
|
|
||||||
lastCommand = newLightState;
|
lastCommand = newLightState;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -248,16 +249,18 @@ public class LightThingHandler extends DeconzBaseThingHandler<LightMessage> {
|
||||||
return null;
|
return null;
|
||||||
} else if (r.getResponseCode() == 200) {
|
} else if (r.getResponseCode() == 200) {
|
||||||
LightMessage lightMessage = gson.fromJson(r.getBody(), LightMessage.class);
|
LightMessage lightMessage = gson.fromJson(r.getBody(), LightMessage.class);
|
||||||
if (lightMessage != null && needsPropertyUpdate) {
|
if (needsPropertyUpdate) {
|
||||||
// if we did not receive an ctmin/ctmax, then we probably don't need it
|
// if we did not receive an ctmin/ctmax, then we probably don't need it
|
||||||
needsPropertyUpdate = false;
|
needsPropertyUpdate = false;
|
||||||
|
|
||||||
if (lightMessage.ctmin != null && lightMessage.ctmax != null) {
|
Integer ctmax = lightMessage.ctmax;
|
||||||
|
Integer ctmin = lightMessage.ctmin;
|
||||||
|
if (ctmin != null && ctmax != null) {
|
||||||
Map<String, String> properties = new HashMap<>(thing.getProperties());
|
Map<String, String> properties = new HashMap<>(thing.getProperties());
|
||||||
properties.put(PROPERTY_CT_MAX,
|
properties.put(PROPERTY_CT_MAX,
|
||||||
Integer.toString(Util.constrainToRange(lightMessage.ctmax, ZCL_CT_MIN, ZCL_CT_MAX)));
|
Integer.toString(Util.constrainToRange(ctmax, ZCL_CT_MIN, ZCL_CT_MAX)));
|
||||||
properties.put(PROPERTY_CT_MIN,
|
properties.put(PROPERTY_CT_MIN,
|
||||||
Integer.toString(Util.constrainToRange(lightMessage.ctmin, ZCL_CT_MIN, ZCL_CT_MAX)));
|
Integer.toString(Util.constrainToRange(ctmin, ZCL_CT_MIN, ZCL_CT_MAX)));
|
||||||
updateProperties(properties);
|
updateProperties(properties);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -278,6 +281,8 @@ public class LightThingHandler extends DeconzBaseThingHandler<LightMessage> {
|
||||||
|
|
||||||
private void valueUpdated(String channelId, LightState newState) {
|
private void valueUpdated(String channelId, LightState newState) {
|
||||||
Integer bri = newState.bri;
|
Integer bri = newState.bri;
|
||||||
|
Integer hue = newState.hue;
|
||||||
|
Integer sat = newState.sat;
|
||||||
Boolean on = newState.on;
|
Boolean on = newState.on;
|
||||||
|
|
||||||
switch (channelId) {
|
switch (channelId) {
|
||||||
|
@ -292,15 +297,13 @@ public class LightThingHandler extends DeconzBaseThingHandler<LightMessage> {
|
||||||
case CHANNEL_COLOR:
|
case CHANNEL_COLOR:
|
||||||
if (on != null && on == false) {
|
if (on != null && on == false) {
|
||||||
updateState(channelId, OnOffType.OFF);
|
updateState(channelId, OnOffType.OFF);
|
||||||
} else if (bri != null && newState.colormode != null && newState.colormode.equals("xy")) {
|
} else if (bri != null && "xy".equals(newState.colormode)) {
|
||||||
final double @Nullable [] xy = newState.xy;
|
final double @Nullable [] xy = newState.xy;
|
||||||
if (xy != null && xy.length == 2) {
|
if (xy != null && xy.length == 2) {
|
||||||
HSBType color = HSBType.fromXY((float) xy[0], (float) xy[1]);
|
HSBType color = HSBType.fromXY((float) xy[0], (float) xy[1]);
|
||||||
updateState(channelId, new HSBType(color.getHue(), color.getSaturation(), toPercentType(bri)));
|
updateState(channelId, new HSBType(color.getHue(), color.getSaturation(), toPercentType(bri)));
|
||||||
}
|
}
|
||||||
} else if (bri != null && newState.hue != null && newState.sat != null) {
|
} else if (bri != null && hue != null && sat != null) {
|
||||||
final Integer hue = newState.hue;
|
|
||||||
final Integer sat = newState.sat;
|
|
||||||
updateState(channelId,
|
updateState(channelId,
|
||||||
new HSBType(new DecimalType(hue / HUE_FACTOR), toPercentType(sat), toPercentType(bri)));
|
new HSBType(new DecimalType(hue / HUE_FACTOR), toPercentType(sat), toPercentType(bri)));
|
||||||
}
|
}
|
||||||
|
@ -340,7 +343,7 @@ public class LightThingHandler extends DeconzBaseThingHandler<LightMessage> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
lightStateCache = lightState;
|
lightStateCache = lightState;
|
||||||
if (lightState.reachable != null && lightState.reachable) {
|
if (Boolean.TRUE.equals(lightState.reachable)) {
|
||||||
updateStatus(ThingStatus.ONLINE);
|
updateStatus(ThingStatus.ONLINE);
|
||||||
thing.getChannels().stream().map(c -> c.getUID().getId()).forEach(c -> valueUpdated(c, lightState));
|
thing.getChannels().stream().map(c -> c.getUID().getId()).forEach(c -> valueUpdated(c, lightState));
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -164,23 +164,31 @@ public abstract class SensorBaseThingHandler extends DeconzBaseThingHandler<Sens
|
||||||
// "Last seen" is the last "ping" from the device, whereas "last update" is the last status changed.
|
// "Last seen" is the last "ping" from the device, whereas "last update" is the last status changed.
|
||||||
// For example, for a fire sensor, the device pings regularly, without necessarily updating channels.
|
// For example, for a fire sensor, the device pings regularly, without necessarily updating channels.
|
||||||
// So to monitor a sensor is still alive, the "last seen" is necessary.
|
// So to monitor a sensor is still alive, the "last seen" is necessary.
|
||||||
|
// Because "last seen" is never updated by the WebSocket API - if this is supported, then we have to
|
||||||
|
// manually poll it after the defined time
|
||||||
String lastSeen = stateResponse.lastseen;
|
String lastSeen = stateResponse.lastseen;
|
||||||
if (lastSeen != null && config.lastSeenPolling > 0) {
|
if (lastSeen != null && config.lastSeenPolling > 0) {
|
||||||
createChannel(CHANNEL_LAST_SEEN, ChannelKind.STATE);
|
createChannel(CHANNEL_LAST_SEEN, ChannelKind.STATE);
|
||||||
updateState(CHANNEL_LAST_SEEN, Util.convertTimestampToDateTime(lastSeen));
|
updateState(CHANNEL_LAST_SEEN, Util.convertTimestampToDateTime(lastSeen));
|
||||||
// Because "last seen" is never updated by the WebSocket API - if this is supported, then we have to
|
lastSeenPollingJob = scheduler.schedule(() -> requestState(this::processLastSeen), config.lastSeenPolling,
|
||||||
// manually poll it after the defined time (default is off)
|
|
||||||
if (config.lastSeenPolling > 0) {
|
|
||||||
lastSeenPollingJob = scheduler.schedule((Runnable) this::requestState, config.lastSeenPolling,
|
|
||||||
TimeUnit.MINUTES);
|
TimeUnit.MINUTES);
|
||||||
logger.trace("lastSeen polling enabled for thing {} with interval of {} minutes", thing.getUID(),
|
logger.trace("lastSeen polling enabled for thing {} with interval of {} minutes", thing.getUID(),
|
||||||
config.lastSeenPolling);
|
config.lastSeenPolling);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
updateStatus(ThingStatus.ONLINE);
|
updateStatus(ThingStatus.ONLINE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void processLastSeen(@Nullable SensorMessage stateResponse) {
|
||||||
|
if (stateResponse == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String lastSeen = stateResponse.lastseen;
|
||||||
|
if (lastSeen != null) {
|
||||||
|
updateState(CHANNEL_LAST_SEEN, Util.convertTimestampToDateTime(lastSeen));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected void createChannel(String channelId, ChannelKind kind) {
|
protected void createChannel(String channelId, ChannelKind kind) {
|
||||||
ThingHandlerCallback callback = getCallback();
|
ThingHandlerCallback callback = getCallback();
|
||||||
if (callback != null) {
|
if (callback != null) {
|
||||||
|
|
|
@ -22,6 +22,8 @@ import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
|
import javax.measure.quantity.Temperature;
|
||||||
|
|
||||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
import org.eclipse.jdt.annotation.Nullable;
|
import org.eclipse.jdt.annotation.Nullable;
|
||||||
import org.openhab.binding.deconz.internal.dto.SensorConfig;
|
import org.openhab.binding.deconz.internal.dto.SensorConfig;
|
||||||
|
@ -125,7 +127,8 @@ public class SensorThermostatThingHandler extends SensorBaseThingHandler {
|
||||||
@Override
|
@Override
|
||||||
protected void valueUpdated(ChannelUID channelUID, SensorConfig newConfig) {
|
protected void valueUpdated(ChannelUID channelUID, SensorConfig newConfig) {
|
||||||
super.valueUpdated(channelUID, newConfig);
|
super.valueUpdated(channelUID, newConfig);
|
||||||
String mode = newConfig.mode != null ? newConfig.mode.name() : ThermostatMode.UNKNOWN.name();
|
ThermostatMode thermostatMode = newConfig.mode;
|
||||||
|
String mode = thermostatMode != null ? thermostatMode.name() : ThermostatMode.UNKNOWN.name();
|
||||||
String channelID = channelUID.getId();
|
String channelID = channelUID.getId();
|
||||||
switch (channelID) {
|
switch (channelID) {
|
||||||
case CHANNEL_HEATSETPOINT:
|
case CHANNEL_HEATSETPOINT:
|
||||||
|
@ -135,9 +138,7 @@ public class SensorThermostatThingHandler extends SensorBaseThingHandler {
|
||||||
updateQuantityTypeChannel(channelID, newConfig.offset, CELSIUS, 1.0 / 100);
|
updateQuantityTypeChannel(channelID, newConfig.offset, CELSIUS, 1.0 / 100);
|
||||||
break;
|
break;
|
||||||
case CHANNEL_THERMOSTAT_MODE:
|
case CHANNEL_THERMOSTAT_MODE:
|
||||||
if (mode != null) {
|
|
||||||
updateState(channelUID, new StringType(mode));
|
updateState(channelUID, new StringType(mode));
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -169,7 +170,13 @@ public class SensorThermostatThingHandler extends SensorBaseThingHandler {
|
||||||
if (command instanceof DecimalType) {
|
if (command instanceof DecimalType) {
|
||||||
newTemperature = ((DecimalType) command).toBigDecimal();
|
newTemperature = ((DecimalType) command).toBigDecimal();
|
||||||
} else if (command instanceof QuantityType) {
|
} else if (command instanceof QuantityType) {
|
||||||
newTemperature = ((QuantityType) command).toUnit(CELSIUS).toBigDecimal();
|
@SuppressWarnings("unchecked")
|
||||||
|
QuantityType<Temperature> temperatureCelsius = ((QuantityType<Temperature>) command).toUnit(CELSIUS);
|
||||||
|
if (temperatureCelsius != null) {
|
||||||
|
newTemperature = temperatureCelsius.toBigDecimal();
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,8 +34,6 @@ import org.openhab.core.thing.ThingTypeUID;
|
||||||
import org.openhab.core.thing.type.ChannelKind;
|
import org.openhab.core.thing.type.ChannelKind;
|
||||||
import org.openhab.core.types.Command;
|
import org.openhab.core.types.Command;
|
||||||
import org.openhab.core.types.RefreshType;
|
import org.openhab.core.types.RefreshType;
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
|
|
||||||
|
@ -65,8 +63,6 @@ public class SensorThingHandler extends SensorBaseThingHandler {
|
||||||
private static final List<String> CONFIG_CHANNELS = Arrays.asList(CHANNEL_BATTERY_LEVEL, CHANNEL_BATTERY_LOW,
|
private static final List<String> CONFIG_CHANNELS = Arrays.asList(CHANNEL_BATTERY_LEVEL, CHANNEL_BATTERY_LOW,
|
||||||
CHANNEL_TEMPERATURE);
|
CHANNEL_TEMPERATURE);
|
||||||
|
|
||||||
private final Logger logger = LoggerFactory.getLogger(SensorThingHandler.class);
|
|
||||||
|
|
||||||
public SensorThingHandler(Thing thing, Gson gson) {
|
public SensorThingHandler(Thing thing, Gson gson) {
|
||||||
super(thing, gson);
|
super(thing, gson);
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,6 @@ import org.eclipse.jdt.annotation.Nullable;
|
||||||
@NonNullByDefault
|
@NonNullByDefault
|
||||||
public class ThingConfig {
|
public class ThingConfig {
|
||||||
public String id = "";
|
public String id = "";
|
||||||
public int lastSeenPolling = 0;
|
public int lastSeenPolling = 1440;
|
||||||
public @Nullable Double transitiontime;
|
public @Nullable Double transitiontime;
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,8 +42,8 @@
|
||||||
<parameter name="lastSeenPolling" type="integer" min="0" unit="min">
|
<parameter name="lastSeenPolling" type="integer" min="0" unit="min">
|
||||||
<label>LastSeen Poll Interval</label>
|
<label>LastSeen Poll Interval</label>
|
||||||
<description>Interval to poll the deCONZ Gateway for this sensor's "lastSeen" channel. Polling is disabled when set
|
<description>Interval to poll the deCONZ Gateway for this sensor's "lastSeen" channel. Polling is disabled when set
|
||||||
to 0.</description>
|
to 1440 (once per day).</description>
|
||||||
<default>0</default>
|
<default>1440</default>
|
||||||
</parameter>
|
</parameter>
|
||||||
</config-description>
|
</config-description>
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue