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}