001package jmri.jmrix.mqtt;
002
003import javax.annotation.Nonnull;
004import jmri.Turnout;
005import jmri.implementation.AbstractTurnout;
006
007/**
008 * Implementation of the Turnout interface for MQTT layouts.
009 *
010 * @author Lionel Jeanson Copyright (c) 2017
011 * @author Bob Jacobsen   Copyright (c) 2020
012 */
013public class MqttTurnout extends AbstractTurnout implements MqttEventListener {
014
015    private final MqttAdapter mqttAdapter;
016    private final String sendTopic;
017    private final String rcvTopic;
018
019    /**
020     * Requires, but does not check, that the system name and topic be consistent
021     * @param ma Adapter to reference for connection
022     * @param systemName System name of turnout
023     * @param sendTopic MQTT topic to use when sending (full string, including systemName part)
024     * @param rcvTopic MQTT topic to use when receiving (full string, including systemName part)
025     */
026    MqttTurnout(MqttAdapter ma, String systemName, String sendTopic, String rcvTopic) {
027        super(systemName);
028        this.sendTopic = sendTopic;
029        this.rcvTopic  = rcvTopic;
030        mqttAdapter = ma;
031        mqttAdapter.subscribe(rcvTopic, this);  // only receive receive topic, not send one
032        _validFeedbackNames = new String[] {"DIRECT", "ONESENSOR", "TWOSENSOR", "DELAYED", "MONITORING"};
033        _validFeedbackModes = new int[] {DIRECT, ONESENSOR, TWOSENSOR, DELAYED, MONITORING};
034        _validFeedbackTypes = DIRECT | ONESENSOR | TWOSENSOR | DELAYED | MONITORING;
035    }
036
037    public void setParser(MqttContentParser<Turnout> parser) {
038        this.parser = parser;
039    }
040        
041    MqttContentParser<Turnout> parser = new MqttContentParser<Turnout>() {
042        // public for scripting
043        public final static String closedText = "CLOSED";
044        public final static String thrownText = "THROWN";
045        public final static String unknownText = "UNKNOWN";
046        public final static String inconsistentText = "INCONSISTENT";
047
048        int stateFromString(String payload) {
049            switch (payload) {
050                case closedText:                
051                    return CLOSED;
052                case thrownText:
053                    return THROWN;
054                case unknownText:
055                    return UNKNOWN;
056                case inconsistentText:
057                    return INCONSISTENT;
058                default:
059                    log.warn("Unknown state : {}, substitute UNKNOWN", payload);
060                    return UNKNOWN;
061            }
062        }
063        
064        @Override
065        public void beanFromPayload(@Nonnull Turnout bean, @Nonnull String payload, @Nonnull String topic) {
066            int state = stateFromString(payload);
067            
068            boolean couldBeSendMessage = topic.endsWith(sendTopic); // not listening for send messages, but can get them anyway
069            boolean couldBeRcvMessage = topic.endsWith(rcvTopic);
070            
071            if (couldBeSendMessage) {
072                // always accept as commadn
073                newCommandedState(state);
074                
075                // when needed, do feedback
076                if (getFeedbackMode() == DIRECT || getFeedbackMode() == MONITORING) newKnownState(state);
077                
078                return;
079            }
080            
081            if (couldBeRcvMessage) {
082
083                // if MONITORING, do feedback
084                if (getFeedbackMode() == DIRECT || getFeedbackMode() == MONITORING) newKnownState(state);
085                
086                return;
087            }
088
089            // really shouldn't have gotten here
090            log.warn("expected failure to decode topic {} {}", topic, payload);
091            return;
092        }
093        
094        @Override
095        public @Nonnull String payloadFromBean(@Nonnull Turnout bean, int newState) {
096            // calls jmri.implementation.AbstractTurnout#stateChangeCheck(int)
097            String text = "";
098            try {
099                text = (stateChangeCheck(newState) ? closedText : thrownText);
100            } catch (IllegalArgumentException ex) {
101                log.error("new state invalid, Turnout not set");
102            }
103            return text;
104        }
105    };
106
107    // MQTT Turnouts do support inversion
108    @Override
109    public boolean canInvert() {
110        return true;
111    }
112
113    /**
114     * {@inheritDoc}
115     * Sends an MQTT payload command
116     */
117    @Override
118    protected void forwardCommandChangeToLayout(int s) {
119        // sort out states
120        String payload = parser.payloadFromBean(this, s);
121
122        // send appropriate command
123        sendMessage(payload);
124    }
125
126    private void sendMessage(String c) {
127        mqttAdapter.publish(sendTopic, c);
128    }
129
130    @Override
131    public void notifyMqttMessage(String receivedTopic, String message) {
132        if (! ( receivedTopic.endsWith(rcvTopic) || receivedTopic.endsWith(sendTopic) ) ) {
133            log.error("Got a message whose topic ({}) wasn't for me ({})", receivedTopic, rcvTopic);
134            return;
135        }
136        
137        parser.beanFromPayload(this, message, receivedTopic);
138    }
139
140    @Override
141    protected void turnoutPushbuttonLockout(boolean _pushButtonLockout) {
142        log.warn("Send command to {} Pushbutton in {} not yet coded", (_pushButtonLockout ? "Lock" : "Unlock"), getSystemName());
143    }
144
145    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(MqttTurnout.class);
146
147}