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 private final String intensityText = MqttLight.intensityText; 038 039 int stateFromString(String payload) { 040 if (payload.startsWith(intensityText)) return -1; // means don't change state 041 switch (payload) { 042 case onText: return ON; 043 case offText: return OFF; 044 default: return UNKNOWN; 045 } 046 } 047 048 @Override 049 public void beanFromPayload(@Nonnull Light bean, @Nonnull String payload, @Nonnull String topic) { 050 log.debug("beanFromPayload {} {} {}", bean, payload, topic); 051 int state = stateFromString(payload); 052 053 if (state == -1) { 054 // don't change anything 055 log.trace(" no changes"); 056 return; 057 } 058 boolean couldBeSendMessage = topic.endsWith(sendTopic); 059 boolean couldBeRcvMessage = topic.endsWith(rcvTopic); 060 061 if (couldBeSendMessage) { 062 log.trace(" setCommandedState {}", state); 063 setCommandedState(state); 064 } else if (couldBeRcvMessage) { 065 setState(state); 066 log.trace(" setState {}", state); 067 } else { 068 log.warn("failure to decode topic {} {}", topic, payload); 069 } 070 } 071 072 @Override 073 public @Nonnull String payloadFromBean(@Nonnull Light bean, int newState){ 074 String toReturn = "UNKNOWN"; 075 switch (getState()) { 076 case Light.ON: 077 toReturn = onText; 078 break; 079 case Light.OFF: 080 toReturn = offText; 081 break; 082 default: 083 log.error("Light has a state which is not supported {}", newState); 084 break; 085 } 086 return toReturn; 087 } 088 }; 089 090 // For AbstractVariableLight 091 @Override 092 protected int getNumberOfSteps() { 093 return 20; 094 } 095 096 // For AbstractVariableLight 097 @Override 098 protected void sendIntensity(double intensity) { 099 sendMessage(intensityText+intensity); 100 } 101 102 // For AbstractVariableLight 103 @Override 104 protected void sendOnOffCommand(int newState) { 105 switch (newState) { 106 case ON: 107 sendMessage(true); 108 break; 109 case OFF: 110 sendMessage(false); 111 break; 112 default: 113 log.error("Unexpected state to sendOnOff: {}", newState); 114 } 115 } 116 117 // Handle a request to change state by sending a formatted packet 118 // to the server. 119 @Override 120 protected void doNewState(int oldState, int newState) { 121 log.debug("doNewState with old state {} new state {}", oldState, newState); 122 if (oldState == newState) { 123 return; //no change, just quit. 124 } // sort out states 125 if ((newState & Light.ON) != 0) { 126 // first look for the double case, which we can't handle 127 if ((newState & Light.OFF) != 0) { 128 // this is the disaster case! 129 log.error("Cannot command both ON and OFF {}", newState); 130 return; 131 } else { 132 // send a ON command 133 sendMessage(true); 134 } 135 } else { 136 // send a OFF command 137 sendMessage(false); 138 } 139 } 140 141 private void sendMessage(boolean on) { 142 this.sendMessage(on ? "ON" : "OFF"); 143 } 144 145 private void sendMessage(String c) { 146 jmri.util.ThreadingUtil.runOnLayoutEventually(() -> { 147 mqttAdapter.publish(this.sendTopic, c.getBytes()); 148 }); 149 log.debug("sent {}", c); 150 } 151 152 @Override 153 public void setState(int newState) { 154 log.debug("setState {} was {}", newState, mState); 155 156 if (newState != ON && newState != OFF && newState != UNKNOWN) { 157 throw new IllegalArgumentException("cannot set state value " + newState); 158 } 159 160 // do the state change in the hardware 161 doNewState(mState, newState); 162 // change value and tell listeners 163 notifyStateChange(mState, newState); 164 } 165 166 //request a status update from the layout 167 @Override 168 public void requestUpdateFromLayout() { 169 } 170 171 @Override 172 public void notifyMqttMessage(String receivedTopic, String message) { 173 if (! ( receivedTopic.endsWith(rcvTopic) || receivedTopic.endsWith(sendTopic) ) ) { 174 log.error("Got a message whose topic ({}) wasn't for me ({})", receivedTopic, rcvTopic); 175 return; 176 } 177 log.debug("notifyMqttMessage with {}", message); 178 179 // parser doesn't support intensity, so first handle that here 180 if (message.startsWith(intensityText)) { 181 var stringValue = message.substring(intensityText.length()); 182 try { 183 double intensity = Double.parseDouble(stringValue); 184 log.debug("setting received intensity with {}", intensity); 185 setObservedAnalogValue(intensity); 186 } catch (NumberFormatException e) { 187 log.warn("could not parse input {}", receivedTopic, e); 188 } 189 } 190 191 // handle on/off 192 parser.beanFromPayload(this, message, receivedTopic); 193 } 194 195 private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(MqttLight.class); 196}