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}