001package jmri.jmrix.mqtt;
002
003import javax.annotation.*;
004import jmri.*;
005import jmri.implementation.AbstractSensor;
006
007/**
008 * Implementation of the Sensor interface for MQTT layouts.
009 *
010 * @author Lionel Jeanson Copyright (c) 2017, 2019
011 * @author Bob Jacobsen   Copyright (c) 2020
012 */
013public class MqttSensor extends AbstractSensor 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 specific connection
022     * @param systemName System Name for this Sensor
023     * @param sendTopic Topic string to be used when sending from JMRI
024     * @param rcvTopic Topic string to be used when receiving by JMRI
025     */
026    MqttSensor(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    }
033
034    public void setParser(MqttContentParser<Sensor> parser) {
035        this.parser = parser;
036    }
037    
038    MqttContentParser<Sensor> parser = new MqttContentParser<Sensor>() {
039        private final static String inactiveText = "INACTIVE";
040        private final static String activeText = "ACTIVE";
041        @Override
042        public void beanFromPayload(@Nonnull Sensor bean, @Nonnull String payload, @Nonnull String topic) {
043            switch (payload) {
044                case inactiveText:                
045                    setOwnState(! _inverted ? INACTIVE : ACTIVE);
046                    break;
047                case activeText:
048                    setOwnState(! _inverted ? ACTIVE : INACTIVE);
049                    break;
050                default:
051                    log.warn("{} saw unknown state : {}", getDisplayName(), payload);
052                    break;
053            }
054        }
055        
056        @Override
057        public @Nonnull String payloadFromBean(@Nonnull Sensor bean, int newState){
058            // sort out states
059            if ((newState & Sensor.INACTIVE) != 0 ^ getInverted()) {
060                // first look for the double case, which we can't handle
061                if ((newState & Sensor.ACTIVE ) != 0 ^ getInverted()) {
062                    // this is the disaster case!
063                    log.error("Cannot command both INACTIVE and ACTIVE: {}", newState);
064                    throw new IllegalArgumentException("Cannot command both INACTIVE and ACTIVE: "+newState);
065                } else {
066                    // send a INACTIVE command
067                    return inactiveText;
068                }
069            } else {
070                // send a ACIVE command
071                return activeText;
072            }
073        }
074    };
075    
076
077    // Sensors do support inversion
078    @Override
079    public boolean canInvert() {
080        return true;
081    }
082
083    /**
084     * The request is just swallowed
085     */
086    @Override
087    public void requestUpdateFromLayout() {}
088
089    // Handle a request to change state by sending MQTT message
090    @Override
091    public void setKnownState(int s) {
092        // sort out states
093        String payload = parser.payloadFromBean(this, s);
094        log.debug("payload: {}", payload);
095        // send appropriate command
096        sendMessage(payload);
097        
098        // and do internal operations
099        setOwnState(s);
100    }
101
102    private void sendMessage(String c) {
103        mqttAdapter.publish(sendTopic, c);
104    }
105
106    @Override
107    public void notifyMqttMessage(String receivedTopic, String message) {
108        if (! ( receivedTopic.endsWith(rcvTopic) || receivedTopic.endsWith(sendTopic) ) ) {
109            log.error("{} got a message whose topic ({}) wasn't for me ({})", getDisplayName(), receivedTopic, rcvTopic);
110            return;
111        }
112        
113        parser.beanFromPayload(this, message, receivedTopic);
114    }
115
116
117    @Override
118    public void dispose() {
119        mqttAdapter.unsubscribe(rcvTopic, this);
120        super.dispose();
121    }
122
123    private final static org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(MqttSensor.class);
124
125}