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}