001package jmri.jmrix.mqtt; 002 003import jmri.Light; 004import jmri.implementation.AbstractVariableLight; 005 006import javax.annotation.Nonnull; 007 008/** 009 * MQTT implementation of the Light interface. 010 * 011 * @author Bob Jacobsen Copyright (C) 2001, 2008, 2020, 2023 012 * @author Paul Bender Copyright (C) 2010 013 * @author Fredrik Elestedt Copyright (C) 2020 014 */ 015public class MqttLight extends AbstractVariableLight implements MqttEventListener { 016 private final MqttAdapter mqttAdapter; 017 private final String sendTopic; 018 private final String rcvTopic; 019 020 static public String intensityText = "INTENSITY "; // public for script access 021 022 public MqttLight(MqttAdapter ma, String systemName, String userName, String sendTopic, String rcvTopic) { 023 super(systemName, userName); 024 this.sendTopic = sendTopic; 025 this.rcvTopic = rcvTopic; 026 this.mqttAdapter = ma; 027 this.mqttAdapter.subscribe(rcvTopic, this); 028 } 029 030 public void setParser(MqttContentParser<Light> parser) { 031 this.parser = parser; 032 } 033 034 MqttContentParser<Light> parser = new MqttContentParser<Light>() { 035 private static final String onText = "ON"; 036 private static final String offText = "OFF"; 037 038 int stateFromString(String payload) { 039 if (payload.startsWith(intensityText)) return -1; // means don't change state 040 switch (payload) { 041 case onText: return ON; 042 case offText: return OFF; 043 default: return UNKNOWN; 044 } 045 } 046 047 @Override 048 public void beanFromPayload(@Nonnull Light bean, @Nonnull String payload, @Nonnull String topic) { 049 log.debug("beanFromPayload {} {} {}", bean, payload, topic); 050 int state = stateFromString(payload); 051 052 if (state == -1) { 053 // don't change anything 054 log.trace(" no changes"); 055 return; 056 } 057 boolean couldBeSendMessage = topic.endsWith(sendTopic); 058 boolean couldBeRcvMessage = topic.endsWith(rcvTopic); 059 060 if (couldBeSendMessage) { 061 log.trace(" setCommandedState {}", state); 062 setCommandedState(state); 063 } else if (couldBeRcvMessage) { 064 setState(state); 065 log.trace(" setState {}", state); 066 } else { 067 log.warn("{} failure to decode topic {} {}", getDisplayName(), topic, payload); 068 } 069 } 070 071 @Override 072 public @Nonnull String payloadFromBean(@Nonnull Light bean, int newState){ 073 String toReturn = "UNKNOWN"; 074 switch (getState()) { 075 case Light.ON: 076 toReturn = onText; 077 break; 078 case Light.OFF: 079 toReturn = offText; 080 break; 081 default: 082 log.error("Light {} has a state which is not supported {}", getDisplayName(), newState); 083 break; 084 } 085 return toReturn; 086 } 087 }; 088 089 // For AbstractVariableLight 090 @Override 091 protected int getNumberOfSteps() { 092 return 20; 093 } 094 095 // For AbstractVariableLight 096 @Override 097 protected void sendIntensity(double intensity) { 098 sendMessage(intensityText+intensity); 099 } 100 101 // For AbstractVariableLight 102 @Override 103 protected void sendOnOffCommand(int newState) { 104 switch (newState) { 105 case ON: 106 sendMessage(true); 107 break; 108 case OFF: 109 sendMessage(false); 110 break; 111 default: 112 log.error("Unexpected state to sendOnOff: {}", newState); 113 } 114 } 115 116 // Handle a request to change state by sending a formatted packet 117 // to the server. 118 @Override 119 protected void doNewState(int oldState, int newState) { 120 log.debug("doNewState with old state {} new state {}", oldState, newState); 121 if (oldState == newState) { 122 return; //no change, just quit. 123 } // sort out states 124 if ((newState & Light.ON) != 0) { 125 // first look for the double case, which we can't handle 126 if ((newState & Light.OFF) != 0) { 127 // this is the disaster case! 128 log.error("Cannot command {} to both ON and OFF {}", getDisplayName(), newState); 129 return; 130 } else { 131 // send a ON command 132 sendMessage(true); 133 } 134 } else { 135 // send a OFF command 136 sendMessage(false); 137 } 138 } 139 140 private void sendMessage(boolean on) { 141 this.sendMessage(on ? "ON" : "OFF"); 142 } 143 144 private void sendMessage(String c) { 145 jmri.util.ThreadingUtil.runOnLayoutEventually(() -> { 146 mqttAdapter.publish(this.sendTopic, c.getBytes()); 147 }); 148 log.debug("sent {}", c); 149 } 150 151 @Override 152 public void setState(int newState) { 153 log.debug("setState {} was {}", newState, mState); 154 155 if (newState != ON && newState != OFF && newState != UNKNOWN) { 156 throw new IllegalArgumentException("cannot set state value " + newState); 157 } 158 159 // do the state change in the hardware 160 doNewState(mState, newState); 161 // change value and tell listeners 162 notifyStateChange(mState, newState); 163 } 164 165 //request a status update from the layout 166 @Override 167 public void requestUpdateFromLayout() { 168 } 169 170 @Override 171 public void notifyMqttMessage(String receivedTopic, String message) { 172 if (! ( receivedTopic.endsWith(rcvTopic) || receivedTopic.endsWith(sendTopic) ) ) { 173 log.error("{} got a message whose topic ({}) wasn't for me ({})", getDisplayName(), receivedTopic, rcvTopic); 174 return; 175 } 176 log.debug("notifyMqttMessage with {}", message); 177 178 // parser doesn't support intensity, so first handle that here 179 if (message.startsWith(intensityText)) { 180 var stringValue = message.substring(intensityText.length()); 181 try { 182 double intensity = Double.parseDouble(stringValue); 183 log.debug("setting received intensity with {}", intensity); 184 setObservedAnalogValue(intensity); 185 } catch (NumberFormatException e) { 186 log.warn("could not parse input {}", receivedTopic, e); 187 } 188 } 189 190 // handle on/off 191 parser.beanFromPayload(this, message, receivedTopic); 192 } 193 194 @Override 195 public void dispose() { 196 mqttAdapter.unsubscribe(rcvTopic,this); 197 super.dispose(); 198 } 199 200 private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(MqttLight.class); 201}