def _on_connect(self, client: Client, user_data, flags, reason_code, properties=None): logger.info(f"Connected with reason code {reason_code}") sub_topic = f"{self.sub_topic_root}/#" logger.info(f"Subscribing to: {sub_topic}") client.subscribe(sub_topic) client.will_set(f"label_servers/status/{self.host_name}", json.dumps({"online": False}))
def create_client(hostname=leader_hostname, last_will=None, client_id=None, keepalive=60): from paho.mqtt.client import Client client = Client(client_id=client_id) if last_will is not None: client.will_set(**last_will) client.connect(hostname, keepalive=keepalive) client.loop_start() return client
class MqttClient(object): """Mqtt通讯封装""" def __init__(self, address): if not isinstance(address, tuple) or len(address) != 2: raise ValueError("Invalid address.") def on_connect(client, userdata, flags, rc): self.handleConnected() def on_message(client, userdata, msg): self.handleMessage(msg.topic, msg.payload) self.client = Mqtt() self.address = address self.client.on_connect = on_connect self.client.on_message = on_message def handleConnected(self): pass def handleMessage(self, topic, payload): pass def publish(self, topic, payload=None, qos=0, retain=False): self.client.publish(topic, payload, qos, retain) def subscribe(self, topic, qos=0): self.client.subscribe(topic, qos) def start(self): self.client.connect_async(self.address[0], self.address[1]) self.client.loop_start() def stop(self): self.client.loop_stop() def username_pw_set(self, username, password=None): self.client.username_pw_set(username, password) def will_set(self, topic, payload=None, qos=0, retain=False): self.client.will_set(topic, payload, qos, retain)
def callback_servidor(mqttc, userdata, msg): ''' maneja la conexión inicial con el servidor hasta que recibe permiso, y las desconexiones inesperadas del servidor desconectando a todos los jugadores ''' if msg.payload == b"SERVER_FAIL": print("SERVER_FAIL: se ha caido el servidor. Por favor, introduzca 0.") mqttc.disconnect() conectado.value = 0 elif msg.payload == b"SERVER_READY": sleep(random() * 10) mqttc.publish(choques + "/servidor/" + userdata[0], payload="CONNECT_REQUEST") elif msg.payload == b"CONNECT_ACCEPT": print("SERVIDOR ACTIVO") mqttc.unsubscribe(choques + "/servidor/exception") mqttc.unsubscribe(choques + "/servidor/" + userdata[0]) mqttc.publish(choques + "/solicitudes", payload=userdata[0]) elif msg.payload == b"USER_EXC": print("Usuario no válido") print("Prueba otro usuario que no este en uso") mqttc.disconnect() nombre_usuario = input("¿nombre usuario? ") sleep(1) mqttc = Client(userdata=[nombre_usuario, 0, 0]) #,clean_session=True) mqttc.message_callback_add(choques + "/servidor/#", callback_servidor) mqttc.message_callback_add(choques + "/partidas/#", callback_partidas) mqttc.message_callback_add(choques + "/jugadores/" + nombre_usuario, callback_jugadores) mqttc.will_set(choques + "/jugadores/" + nombre_usuario, payload="DISCONNECT") mqttc.connect(broker) mqttc.subscribe(choques + "/jugadores/" + nombre_usuario) mqttc.subscribe(choques + "/servidor") mqttc.subscribe(choques + "/servidor/" + nombre_usuario) mqttc.subscribe(choques + "/servidor/exception") mqttc.publish(choques + "/servidor/" + nombre_usuario, payload="CONNECT_REQUEST") mqttc.loop_start()
def create_client( hostname: str = leader_hostname, last_will: Optional[dict] = None, client_id: Optional[str] = None, keepalive=60, max_retries=3, ) -> Client: """ Create a MQTT client and connect to a host. """ def on_connect(client: Client, userdata, flags, rc: int, properties=None): if rc > 1: from pioreactor.logging import create_logger logger = create_logger("pubsub.create_client", to_mqtt=False) logger.error(f"Connection failed with error code {rc=}: {connack_string(rc)}") client = Client(client_id=client_id) client.on_connect = on_connect if last_will is not None: client.will_set(**last_will) for retries in range(1, max_retries + 1): try: client.connect(hostname, keepalive=keepalive) except (socket.gaierror, OSError): if retries == max_retries: break time.sleep(retries * 2) else: client.loop_start() break return client
userdata={}) #diccionario como userdata para la info del juego #'info' indica el estado de la partida: #estado: 0 es sin empezar,1 en espera,2 jugando,3 en recuento #alfabeto: las letras que quedan por jugar, de inicio ya están desordenadas #confirmados para tener una forma de ver si todos envian la info #funciones callback: mqttc.message_callback_add(choques + "/jugadores/#", callback_jugadores) mqttc.message_callback_add(choques + "/partidas/#", callback_partidas) mqttc.message_callback_add(choques + "/servidor/#", callback_servidor) mqttc.message_callback_add(choques + "/solicitudes/#", callback_solicitudes) #will_set: #ultimo mensaje que se envía si el Client se desconecta sin usar disconnect() mqttc.will_set(choques + "/servidor", payload="SERVER_FAIL") mqttc.connect(broker) mqttc.publish(choques + "/servidor", payload="SERVER_READY") print("\nSERVIDOR ACTIVO...") print("\nDATOS del juego") print("-nº mínimo de jugadores por partida:", min_jugadores_partida) print("-nº máximo de jugadores por partida:", max_jugadores_partida) print("-puntuación necesaria para ganar:", max_puntuacion) #suscripciones iniciales del servidor mqttc.subscribe(choques + "/servidor/#") mqttc.subscribe(choques + "/solicitudes/#") mqttc.subscribe(choques + "/jugadores/#") mqttc.subscribe(choques + "/partidas/#")
class LampiApp(App): _updatingUI = False _hue = NumericProperty() _saturation = NumericProperty() _brightness = NumericProperty() lamp_is_on = BooleanProperty() just_turned_on = True def _get_hue(self): return self._hue def _set_hue(self, value): self._hue = value def _get_saturation(self): return self._saturation def _set_saturation(self, value): self._saturation = value def _get_brightness(self): return self._brightness def _set_brightness(self, value): self._brightness = value hue = AliasProperty(_get_hue, _set_hue, bind=['_hue']) saturation = AliasProperty(_get_saturation, _set_saturation, bind=['_saturation']) brightness = AliasProperty(_get_brightness, _set_brightness, bind=['_brightness']) gpio17_pressed = BooleanProperty(False) def on_start(self): self._publish_clock = None self.mqtt = Client(client_id=MQTT_CLIENT_ID, protocol=MQTT_VERSION) self.mqtt.on_connect = self.on_connect self.mqtt.connect(MQTT_BROKER_HOST, port=MQTT_BROKER_PORT, keepalive=MQTT_BROKER_KEEP_ALIVE_SECS) self.mqtt.will_set('lamp/connection/lamp_ui/state', "0", qos=2, retain=True) self.mqtt.publish('lamp/connection/lamp_ui/state', "1", qos=2, retain=True) self.mqtt.loop_start() self.set_up_GPIO_and_IP_popup() def on_hue(self, instance, value): if self._updatingUI: return if self._publish_clock is None: self._publish_clock = Clock.schedule_once( lambda dt: self._update_leds(), 0.01) def on_saturation(self, instance, value): if self._updatingUI: return if self._publish_clock is None: self._publish_clock = Clock.schedule_once( lambda dt: self._update_leds(), 0.01) def on_brightness(self, instance, value): if self._updatingUI: return if self._publish_clock is None: self._publish_clock = Clock.schedule_once( lambda dt: self._update_leds(), 0.01) def on_lamp_is_on(self, instance, value): if self._updatingUI: return if self._publish_clock is None: self._publish_clock = Clock.schedule_once( lambda dt: self._update_leds(), 0.01) def on_connect(self, client, userdata, flags, rc): self.mqtt.subscribe(TOPIC_LAMP_CHANGE_NOTIFICATION) self.mqtt.message_callback_add(TOPIC_LAMP_CHANGE_NOTIFICATION, self.receive_new_lamp_state) def receive_new_lamp_state(self, client, userdata, message): new_state = json.loads(message.payload) Clock.schedule_once(lambda dt: self._update_ui(new_state), 0.01) def _update_ui(self, new_state): self._updatingUI = True try: if 'client' in new_state: if new_state['client'] == 'lamp_ui' and not self.just_turned_on: return if 'color' in new_state: self.hue = new_state['color']['h'] self.saturation = new_state['color']['s'] if 'brightness' in new_state: self.brightness = new_state['brightness'] if 'on' in new_state: self.lamp_is_on = new_state['on'] finally: self._updatingUI = False def _update_leds(self): msg = {'color': {'h': self._hue, 's': self._saturation}, 'brightness': self._brightness, 'on': self.lamp_is_on, 'client': 'lamp_ui'} self.mqtt.publish(TOPIC_SET_LAMP_CONFIG, json.dumps(msg), qos = 1) self._publish_clock = None def set_up_GPIO_and_IP_popup(self): self.pi = pigpio.pi() self.pi.set_mode(17, pigpio.INPUT) self.pi.set_pull_up_down(17, pigpio.PUD_UP) Clock.schedule_interval(self._poll_GPIO, 0.05) self.popup = Popup(title='IP Addresses', content=Label(text='IP ADDRESS WILL GO HERE'), size_hint=(1, 1), auto_dismiss=False) self.popup.bind(on_open=self.update_popup_ip_address) def update_popup_ip_address(self, instance): interface = "wlan0" ipaddr = lampi_util.get_ip_address(interface) instance.content.text = "{}: {}".format(interface, ipaddr) def on_gpio17_pressed(self, instance, value): if value: self.popup.open() else: self.popup.dismiss() def _poll_GPIO(self, dt): # GPIO17 is the rightmost button when looking front of LAMPI self.gpio17_pressed = not self.pi.read(17)
nombre_usuario = input("¿nombre usuario? ") #identificador del usuario mqttc = Client(userdata=[nombre_usuario, 0, 0]) #userdata=[nombre_usuario,puntos_usuario,numero_partida_usuario] #funciones callback: #redirigimos los mensajes según el topic del que vengan para mayor claridad mqttc.message_callback_add(choques + "/servidor/#", callback_servidor) mqttc.message_callback_add(choques + "/partidas/#", callback_partidas) mqttc.message_callback_add(choques + "/jugadores/" + nombre_usuario, callback_jugadores) #will_set: #ultimo mensaje que se envía si el Client se desconecta sin usar disconnect() mqttc.will_set(choques + "/jugadores/" + nombre_usuario, payload="DISCONNECT") mqttc.connect(broker) #suscripciones iniciales del cliente mqttc.subscribe(choques + "/jugadores/" + nombre_usuario) mqttc.subscribe(choques + "/servidor") mqttc.subscribe(choques + "/servidor/" + nombre_usuario) mqttc.subscribe(choques + "/servidor/exception") #publicación inicial para unirse al juego mqttc.publish(choques + "/servidor/" + nombre_usuario, payload="CONNECT_REQUEST") print("ESPERANDO AL SERVIDOR...") mqttc.loop_start()
class Mqtt(): def __init__(self, app=None): # type: (Flask) -> None self.app = app self.client = Client() self.client.on_connect = self._handle_connect self.client.on_disconnect = self._handle_disconnect self.topics = [] # type: List[str] self.connected = False if app is not None: self.init_app(app) def init_app(self, app): # type: (Flask) -> None self.username = app.config.get('MQTT_USERNAME') self.password = app.config.get('MQTT_PASSWORD') self.broker_url = app.config.get('MQTT_BROKER_URL', 'localhost') self.broker_port = app.config.get('MQTT_BROKER_PORT', 1883) self.tls_enabled = app.config.get('MQTT_TLS_ENABLED', False) self.keepalive = app.config.get('MQTT_KEEPALIVE', 60) self.last_will_topic = app.config.get('MQTT_LAST_WILL_TOPIC') self.last_will_message = app.config.get('MQTT_LAST_WILL_MESSAGE') self.last_will_qos = app.config.get('MQTT_LAST_WILL_QOS', 0) self.last_will_retain = app.config.get('MQTT_LAST_WILL_RETAIN', False) if self.tls_enabled: self.tls_ca_certs = app.config['MQTT_TLS_CA_CERTS'] self.tls_certfile = app.config.get('MQTT_TLS_CERTFILE') self.tls_keyfile = app.config.get('MQTT_TLS_KEYFILE') self.tls_cert_reqs = app.config.get('MQTT_TLS_CERT_REQS', ssl.CERT_REQUIRED) self.tls_version = app.config.get('MQTT_TLS_VERSION', ssl.PROTOCOL_TLSv1) self.tls_ciphers = app.config.get('MQTT_TLS_CIPHERS') self.tls_insecure = app.config.get('MQTT_TLS_INSECURE', False) # set last will message if self.last_will_topic is not None: self.client.will_set(self.last_will_topic, self.last_will_message, self.last_will_qos, self.last_will_retain) self.app = app self._connect() def _connect(self): # type: () -> None if self.username is not None: self.client.username_pw_set(self.username, self.password) # security if self.tls_enabled: if self.tls_insecure: self.client.tls_insecure_set(self.tls_insecure) self.client.tls_set( ca_certs=self.tls_ca_certs, certfile=self.tls_certfile, keyfile=self.tls_keyfile, cert_reqs=self.tls_cert_reqs, tls_version=self.tls_version, ciphers=self.tls_ciphers, ) self.client.loop_start() res = self.client.connect(self.broker_url, self.broker_port, keepalive=self.keepalive) def _disconnect(self): # type: () -> None self.client.loop_stop() self.client.disconnect() def _handle_connect(self, client, userdata, flags, rc): # type: (Client, Any, Dict, int) -> None if rc == MQTT_ERR_SUCCESS: self.connected = True for topic in self.topics: self.client.subscribe(topic) def _handle_disconnect(self, client, userdata, rc): # type: (str, Any, int) -> None self.connected = False def on_topic(self, topic): # type: (str) -> Callable """ Decorator to add a callback function that is called when a certain topic has been published. The callback function is expected to have the following form: `handle_topic(client, userdata, message)` :parameter topic: a string specifying the subscription topic to subscribe to The topic still needs to be subscribed via mqtt.subscribe() before the callback function can be used to handle a certain topic. This way it is possible to subscribe and unsubscribe during runtime. **Example usage:**:: app = Flask(__name__) mqtt = Mqtt(app) mqtt.subscribe('home/mytopic') @mqtt.on_topic('home/mytopic') def handle_mytopic(client, userdata, message): print('Received message on topic {}: {}' .format(message.topic, message.payload.decode())) """ def decorator(handler): # type: (Callable[[str], None]) -> Callable[[str], None] self.client.message_callback_add(topic, handler) return handler return decorator def subscribe(self, topic, qos=0): # type: (str, int) -> tuple(int, int) """ Subscribe to a certain topic. :param topic: a string specifying the subscription topic to subscribe to. :param qos: the desired quality of service level for the subscription. Defaults to 0. :rtype: (int, int) :result: (result, mid) A topic is a UTF-8 string, which is used by the broker to filter messages for each connected client. A topic consists of one or more topic levels. Each topic level is separated by a forward slash (topic level separator). The function returns a tuple (result, mid), where result is MQTT_ERR_SUCCESS to indicate success or (MQTT_ERR_NO_CONN, None) if the client is not currently connected. mid is the message ID for the subscribe request. The mid value can be used to track the subscribe request by checking against the mid argument in the on_subscribe() callback if it is defined. **Topic example:** `myhome/groundfloor/livingroom/temperature` """ # TODO: add support for list of topics # don't subscribe if already subscribed # try to subscribe result, mid = self.client.subscribe(topic, qos) # if successful add to topics if result == MQTT_ERR_SUCCESS: if topic not in self.topics: self.topics.append(topic) return (result, mid) def unsubscribe(self, topic): # type: (str) -> tuple(int, int) """ Unsubscribe from a single topic. :param topic: a single string that is the subscription topic to unsubscribe from :rtype: (int, int) :result: (result, mid) Returns a tuple (result, mid), where result is MQTT_ERR_SUCCESS to indicate success or (MQTT_ERR_NO_CONN, None) if the client is not currently connected. mid is the message ID for the unsubscribe request. The mid value can be used to track the unsubscribe request by checking against the mid argument in the on_unsubscribe() callback if it is defined. """ # don't unsubscribe if not in topics if topic not in self.topics: return result, mid = self.client.unsubscribe(topic) # if successful remove from topics if result == MQTT_ERR_SUCCESS: self.topics.remove(topic) return result, mid def unsubscribe_all(self): # type: () -> None """ Unsubscribe from all topics. """ topics = self.topics[:] for topic in topics: self.unsubscribe(topic) def publish(self, topic, payload=None, qos=0, retain=False): # type: (str, bytes, int, bool) -> Tuple[int, int] """ Send a message to the broker. :param topic: the topic that the message should be published on :param payload: the actual message to send. If not given, or set to None a zero length message will be used. Passing an int or float will result in the payload being converted to a string representing that number. If you wish to send a true int/float, use struct.pack() to create the payload you require. :param qos: the quality of service level to use :param retain: if set to True, the message will be set as the "last known good"/retained message for the topic :returns: Returns a tuple (result, mid), where result is MQTT_ERR_SUCCESS to indicate success or MQTT_ERR_NO_CONN if the client is not currently connected. mid is the message ID for the publish request. """ if not self.connected: self.client.reconnect() return self.client.publish(topic, payload, qos, retain) def on_message(self): # type: () -> Callable """ Decorator to handle all messages that have been subscribed and that are not handled via the `on_message` decorator. **Note:** Unlike as written in the paho mqtt documentation this callback will not be called if there exists an topic-specific callback added by the `on_topic` decorator. **Example Usage:**:: @mqtt.on_message() def handle_messages(client, userdata, message): print('Received message on topic {}: {}' .format(message.topic, message.payload.decode())) """ def decorator(handler): # type: (Callable) -> Callable self.client.on_message = handler return handler return decorator def on_publish(self): """ Decorator to handle all messages that have been published by the client. **Example Usage:**:: @mqtt.on_publish() def handle_publish(client, userdata, mid): print('Published message with mid {}.' .format(mid)) """ def decorator(handler): self.client.on_publish = handler return handler return decorator def on_subscribe(self): """ Decorator to handle subscribe callbacks. **Usage:**:: @mqtt.on_subscribe() def handle_subscribe(client, userdata, mid, granted_qos): print('Subscription id {} granted with qos {}.' .format(mid, granted_qos)) """ def decorator(handler): self.client.on_subscribe = handler return handler return decorator def on_unsubscribe(self): """ Decorator to handle unsubscribe callbacks. **Usage:**:: @mqtt.unsubscribe() def handle_unsubscribe(client, userdata, mid) print('Unsubscribed from topic (id: {})' .format(mid)') """ def decorator(handler): self.client.on_unsubscribe = handler return handler return decorator def on_log(self): # type: () -> Callable """ Decorator to handle MQTT logging. **Example Usage:** :: @mqtt.on_log() def handle_logging(client, userdata, level, buf): print(client, userdata, level, buf) """ def decorator(handler): # type: (Callable) -> Callable self.client.on_log = handler return handler return decorator
class MqttConnection: def __init__(self, ip, port, username, password, cafile, connection_callback): self.logger = logging.getLogger("mqtt") self.mqtt = Client() if username is not None: self.mqtt.username_pw_set(username, password) if cafile is not None: self.mqtt.tls_set(ca_certs=cafile) self.mqtt.will_set("chromecast/maintenance/_bridge/online", payload="false", retain=True) self.mqtt.on_connect = self._on_connect self.mqtt.on_message = self._on_message self.ip = ip self.port = int(port) self.connection_callback = connection_callback self.queue = [] def _on_connect(self, client, userdata, flags, rc): """ The callback for when the client receives a CONNACK response from the server. """ self.logger.debug("connected to mqtt with result code %d" % rc) self.mqtt.publish("chromecast/maintenance/_bridge/online", "true", retain=True) # subscribing in on_connect() means that if we lose the connection and # reconnect then subscriptions will be renewed. self.connection_callback.on_mqtt_connected(self) if len(self.queue) > 0: self.logger.debug("found %d queued messages" % len(self.queue)) for msg in self.queue: self._internal_send_message(msg[0], msg[1], False) self.queue.clear() self.logger.debug("handled all queued messages") def _on_message(self, client, userdata, msg): """ The callback for when a PUBLISH message is received from the server. """ self.logger.debug("received mqtt publish of %s with data \"%s\"" % (msg.topic, msg.payload)) self.connection_callback.on_mqtt_message_received( msg.topic, msg.payload) def send_message(self, topic, payload): return self._internal_send_message(topic, payload, True) def subscribe(self, topic): self.logger.debug("subscribing to topic %s" % topic) result = self.mqtt.subscribe(topic) if result[0] == MQTT_ERR_NO_CONN: self.logger.warning( "no connection while trying to subscribe to topic %s" % topic) return False return result[0] == MQTT_ERR_SUCCESS def unsubscribe(self, topic): self.logger.debug("unsubscribing from topic %s" % topic) result = self.mqtt.unsubscribe(topic) if result[0] == MQTT_ERR_NO_CONN: self.logger.warning( "no connection while trying to unsubscribe from topic %s" % topic) return False return result[0] == MQTT_ERR_SUCCESS def _internal_send_message(self, topic, payload, queue): self.logger.debug("sending topic %s with value \"%s\"" % (topic, payload)) result = self.mqtt.publish(topic, payload, retain=True) if result == MQTT_ERR_NO_CONN and queue: self.logger.debug( "no connection, saving message with topic %s to queue" % topic) self.queue.append([topic, payload]) elif result[0] != MQTT_ERR_SUCCESS: self.logger.warn("failed sending message %s, mqtt error %s" % (topic, result)) return False return True def start_connection(self): try: self.mqtt.connect(self.ip, self.port) except ConnectionError: self.logger.exception("failed connecting to mqtt") return False self.mqtt.loop_start() return True def stop_connection(self): self.mqtt.publish("chromecast/maintenance/_bridge/online", "false", retain=True) self.mqtt.disconnect() self.mqtt.loop_stop()
class LampiApp(App): _updated = False _updatingUI = False _hue = NumericProperty() _saturation = NumericProperty() _brightness = NumericProperty() _preset = NumericProperty() lamp_is_on = BooleanProperty() _preset_color = NumericProperty() _preset_temp = NumericProperty() remote_connection = StringProperty("[b]Connected:[/b] No") trusted_remotes = StringProperty("[b]Trusted Remotes:[/b] None") mp = Mixpanel(MIXPANEL_TOKEN, consumer=AsyncBufferedConsumer()) def _get_hue(self): return self._hue def _set_hue(self, value): self._hue = value def _get_saturation(self): return self._saturation def _set_saturation(self, value): self._saturation = value def _get_brightness(self): return self._brightness def _set_brightness(self, value): self._brightness = value def _get_preset(self): return self._preset def _set_preset(self, value): self._preset = value def _get_preset_color(self): return self._preset_color def _set_preset_color(self, value): self._preset_color = value def _get_preset_temp(self): return self._preset_temp def _set_preset_temp(self, value): self._preset_temp = value hue = AliasProperty(_get_hue, _set_hue, bind=['_hue']) saturation = AliasProperty(_get_saturation, _set_saturation, bind=['_saturation']) brightness = AliasProperty(_get_brightness, _set_brightness, bind=['_brightness']) preset = AliasProperty(_get_preset, _set_preset, bind=['_preset']) preset_color = AliasProperty(_get_preset_color, _set_preset_color, bind=['_preset_color']) preset_temp = AliasProperty(_get_preset_temp, _set_preset_temp, bind=['_preset_temp']) gpio17_pressed = BooleanProperty(False) device_associated = BooleanProperty(True) def on_start(self): self._publish_clock = None self.mqtt_broker_bridged = False self._associated = True self.association_code = None self.mqtt = Client(client_id=MQTT_CLIENT_ID) self.mqtt.enable_logger() self.mqtt.will_set(client_state_topic(MQTT_CLIENT_ID), "0", qos=2, retain=True) self.mqtt.on_connect = self.on_connect self.mqtt.connect(MQTT_BROKER_HOST, port=MQTT_BROKER_PORT, keepalive=MQTT_BROKER_KEEP_ALIVE_SECS) self.mqtt.loop_start() self.set_up_GPIO_and_device_status_popup() self.associated_status_popup = self._build_associated_status_popup() self.associated_status_popup.bind(on_open=self.update_popup_associated) self._remote = None self._popup_remote = None self.pairing_popup = self._build_pairing_popup() self._update_remotes_ui() self.discoverswitch = self.root.ids.discoverswitch self.discoverswitch.bind(active=self.toggle_discovery) Clock.schedule_interval(self._poll_associated, 0.1) def _build_associated_status_popup(self): return Popup(title='Associate your Lamp', content=Label(text='Msg here', font_size='30sp'), size_hint=(1, 1), auto_dismiss=False) def _build_pairing_popup(self): layout = StackLayout() label = Label( text= 'A new remote is attempting\nto connect to your lamp.\n\nWould you like to\nallow it?', size_hint=(1, None), padding=(4, 4)) label.bind(size=self._update_textsize) deny = Button(text='Deny', size_hint=(0.49, None), height=40) allow = Button(text='Trust', size_hint=(0.49, None), height=40) allow.on_release = self._allow_remote deny.on_release = self._decline_remote layout.add_widget(label) layout.add_widget(Label(size_hint=(1, None), height=15)) layout.add_widget(deny) layout.add_widget(Label(size_hint=(0.02, None), height=1)) layout.add_widget(allow) return Popup(title='Remote Pairing Request', content=layout, size_hint=(1, 0.68), auto_dismiss=False) def _update_textsize(self, instance, value): instance.text_size = (value[0], value[1]) def on_new_remote(self, client, userdata, message): isEmpty = message.payload == b'' if isEmpty: self._remote = None else: remote = json.loads(message.payload.decode('utf-8')) self._remote = remote self._popup_remote = remote if (not remote['allowed']): self.pairing_popup.open() self._update_remotes_ui() def _allow_remote(self): print("Pairing allowed for {}".format(self._popup_remote['address'])) remotes.saveAddress(self._popup_remote['address']) self._remote = None self._popup_remote = None self.pairing_popup.dismiss() self._update_remotes_ui() # Display confirmation conf = Popup( title='Remote Trusted', content=Label( text= 'You have successfully trusted\nyour remote. Pair it again to\nuse it' ), size_hint=(1, 0.5), auto_dismiss=False) conf.open() Clock.schedule_once(lambda dt: conf.dismiss(), 3) def _decline_remote(self): print("Pairing denied for {}".format(self._popup_remote['address'])) self._popup_remote = None self._remote = None self.pairing_popup.dismiss() self._update_remotes_ui() def clear_remotes(self): remotes.clear() self.mqtt.publish(DISCONNECT_TOPIC, b'') self._update_remotes_ui() def toggle_discovery(self, instance, value): # Send message accordingly self.mqtt.publish(DISCOVERY_TOPIC, ("true" if value else "false").encode('utf8'), retain=True) def _update_remotes_ui(self): savedremotes = remotes._read() statustext = "[b]Connected:[/b] False\n\n" if (self._remote is not None): self.remote_connection = "[b]Connected:[/b] [color=32ff32]{}[/color]".format( self._remote['address']) else: self.remote_connection = "[b]Connected:[/b] [color=ff3232]Not connected[/color]" if (len(savedremotes) == 0): self.trusted_remotes = "[b]Trusted Remotes:[/b] None" else: self.trusted_remotes = "[b]Trusted Remotes:[/b]\n" + "\n".join( [" • {}".format(addr) for addr in savedremotes]) def on_hue(self, instance, value): if self._updatingUI: return self._track_ui_event('Slider Change', { 'slider': 'hue-slider', 'value': value }) if self._publish_clock is None: self._publish_clock = Clock.schedule_once( lambda dt: self._update_leds(), 0.01) def on_saturation(self, instance, value): if self._updatingUI: return self._track_ui_event('Slider Change', { 'slider': 'saturation-slider', 'value': value }) if self._publish_clock is None: self._publish_clock = Clock.schedule_once( lambda dt: self._update_leds(), 0.01) def on_brightness(self, instance, value): if self._updatingUI: return self._track_ui_event('Slider Change', { 'slider': 'brightness-slider', 'value': value }) if self._publish_clock is None: self._publish_clock = Clock.schedule_once( lambda dt: self._update_leds(), 0.01) def on_lamp_is_on(self, instance, value): if self._updatingUI: return self._track_ui_event('Toggle Power', {'isOn': value}) if self._publish_clock is None: self._publish_clock = Clock.schedule_once( lambda dt: self._update_leds(), 0.01) def on_preset_temp(self, instance, value): if self._updatingUI: return self._track_ui_event('Slider Change', { 'slider': 'preset_hue_slider', 'value': value }) def _track_ui_event(self, event_name, additional_props={}): device_id = lampi.lampi_util.get_device_id() event_props = { 'event_type': 'ui', 'interface': 'lampi', 'device_id': device_id } event_props.update(additional_props) self.mp.track(device_id, event_name, event_props) def on_connect(self, client, userdata, flags, rc): self.mqtt.publish(client_state_topic(MQTT_CLIENT_ID), b"1", qos=2, retain=True) self.mqtt.message_callback_add(TOPIC_LAMP_CHANGE_NOTIFICATION, self.receive_new_lamp_state) self.mqtt.message_callback_add(broker_bridge_connection_topic(), self.receive_bridge_connection_status) self.mqtt.message_callback_add(TOPIC_LAMP_ASSOCIATED, self.receive_associated) self.mqtt.message_callback_add(NEW_REMOTE_TOPIC, self.on_new_remote) self.mqtt.subscribe(broker_bridge_connection_topic(), qos=1) self.mqtt.subscribe(TOPIC_LAMP_CHANGE_NOTIFICATION, qos=1) self.mqtt.subscribe(TOPIC_LAMP_ASSOCIATED, qos=2) self.mqtt.subscribe(NEW_REMOTE_TOPIC, qos=2) def _poll_associated(self, dt): # this polling loop allows us to synchronize changes from the # MQTT callbacks (which happen in a different thread) to the # Kivy UI self.device_associated = self._associated def receive_associated(self, client, userdata, message): # this is called in MQTT event loop thread new_associated = json.loads(message.payload.decode('utf-8')) if self._associated != new_associated['associated']: if not new_associated['associated']: self.association_code = new_associated['code'] else: self.association_code = None self._associated = new_associated['associated'] def on_device_associated(self, instance, value): if value: self.associated_status_popup.dismiss() else: self.associated_status_popup.open() def update_popup_associated(self, instance): code = self.association_code[0:6] instance.content.text = ("Please use the\n" "following code\n" "to associate\n" "your device\n" "on the Web\n{}".format(code)) def receive_bridge_connection_status(self, client, userdata, message): # monitor if the MQTT bridge to our cloud broker is up if message.payload == b"1": self.mqtt_broker_bridged = True else: self.mqtt_broker_bridged = False def receive_new_lamp_state(self, client, userdata, message): new_state = json.loads(message.payload.decode('utf-8')) Clock.schedule_once(lambda dt: self._update_ui(new_state), 0.01) def _update_ui(self, new_state): if self._updated and new_state['client'] == MQTT_CLIENT_ID: # ignore updates generated by this client, except the first to # make sure the UI is syncrhonized with the lamp_service return self._updatingUI = True try: if 'color' in new_state: self.hue = new_state['color']['h'] self.saturation = new_state['color']['s'] if 'brightness' in new_state: self.brightness = new_state['brightness'] if 'on' in new_state: self.lamp_is_on = new_state['on'] finally: self._updatingUI = False self._updated = True def _update_leds(self): msg = { 'color': { 'h': self._hue, 's': self._saturation }, 'brightness': self._brightness, 'on': self.lamp_is_on, 'client': MQTT_CLIENT_ID } self.mqtt.publish(TOPIC_SET_LAMP_CONFIG, json.dumps(msg).encode('utf-8'), qos=1) self._publish_clock = None def set_up_GPIO_and_device_status_popup(self): self.pi = pigpio.pi() self.pi.set_mode(17, pigpio.INPUT) self.pi.set_pull_up_down(17, pigpio.PUD_UP) Clock.schedule_interval(self._poll_GPIO, 0.05) self.network_status_popup = self._build_network_status_popup() self.network_status_popup.bind(on_open=self.update_device_status_popup) def _build_network_status_popup(self): return Popup(title='Device Status', content=Label(text='IP ADDRESS WILL GO HERE'), size_hint=(1, 1), auto_dismiss=False) def update_device_status_popup(self, instance): interface = "wlan0" ipaddr = lampi.lampi_util.get_ip_address(interface) deviceid = lampi.lampi_util.get_device_id() msg = ("Version: {}\n" "{}: {}\n" "DeviceID: {}\n" "Broker Bridged: {}\n" "Async Analytics").format(LAMPI_APP_VERSION, interface, ipaddr, deviceid, self.mqtt_broker_bridged) instance.content.text = msg def on_gpio17_pressed(self, instance, value): if value: self.network_status_popup.open() else: self.network_status_popup.dismiss() def _poll_GPIO(self, dt): # GPIO17 is the rightmost button when looking front of LAMPI self.gpio17_pressed = not self.pi.read(17) def write_preset(self, num): filewrite = { "stateList": [ { "state": { "h": self._preset_color, "s": 1.0, "b": 1.0 }, "smooth": False, "waitTime": 0, "transitionTime": 0 }, ], 'loop': False } with open(PRESETS[num - 1] + ".json", "w") as f: json.dump(filewrite, f)
class Co2miniMqtt: def __init__(self, config="config.yml"): self.config = yaml.load(open(config, 'r').read()) self.prefix = "/".join( (self.config.get('mqtt', {}).get('discovery_prefix', 'homeassistant'), 'sensor', platform.node(), self.config.get('object_id', 'co2mini'))) self.sensor = None self.mqttc = None self.state = {} self.disc_config = {} self.alive = True self.last_pub = {} def on_connect(self, *args, **kwargs): self.mqttc.publish("{}/config".format(self.prefix), json.dumps(self.disc_config), retain=True) self.mqttc.publish("{}/status".format(self.prefix), 'online', retain=True) def main(self): self.mqttc = Client() self.mqttc.will_set("{}/status".format(self.prefix), 'offline', retain=True) self.mqttc.enable_logger() self.mqttc.username_pw_set( self.config.get('mqtt', {}).get('username', 'homeassistant'), self.config.get('mqtt', {}).get('password', '')) self.mqttc.on_connect = self.on_connect self.disc_config = { 'state_topic': "{}/state".format(self.prefix), #'expire_after': 10, 'name': self.config.get('name', '{} co2mini'.format(platform.node())), 'unit_of_measurement': 'ppm', 'icon': 'mdi:periodic-table-co2', 'availability_topic': "{}/status".format(self.prefix), 'payload_available': "online", 'payload_not_available': "offline", 'json_attributes_topic': "{}/attributes".format(self.prefix), 'force_update': self.config.get('force_update', False), 'unique_id': '{}:{}'.format(platform.node(), self.config.get('device', '/dev/co2mini0')), 'device': { 'connections': [ ['usb', self.config.get('device', '/dev/co2mini0')], ], 'identifiers': (platform.node()), 'manufacturer': 'Various', 'model': 'co2mini', } } self.sensor = CO2Meter(self.config.get('device', '/dev/co2mini0'), self.sensor_callback) self.mqttc.connect(self.config.get('mqtt', {}).get('broker', '127.0.0.1'), port=self.config.get('mqtt', {}).get('port', 1883), keepalive=self.config.get('mqtt', {}).get('keepalive', 60)) self.mqttc.loop_start() while self.alive: self.alive = False time.sleep(10) def sensor_callback(self, sensor, value): self.state[SOURCE_MAP[sensor]] = value pub = { 'state': self.state.get('co2'), 'attributes': json.dumps( {k: v for (k, v) in self.state.items() if not k == 'co2'}), } for k, v in pub.items(): if v != self.last_pub.get(k, None): self.mqttc.publish("{}/{}".format(self.prefix, k), v, retain=True) self.last_pub[k] = v self.alive = True
class Messenger(object): """ MQTT client for Herald transport. """ def __init__(self, peer): """ Initialize client :param peer: The peer behind the MQTT client. :return: """ self.__peer = peer self.__mqtt = MqttClient() self.__mqtt.on_connect = self._on_connect self.__mqtt.on_disconnect = self._on_disconnect self.__mqtt.on_message = self._on_message self.__callback_handler = None self.__WILL_TOPIC = "/".join( (TOPIC_PREFIX, peer.app_id, RIP_TOPIC)) def __make_uid_topic(self, subtopic): """ Constructs a complete UID topic. :param subtopic: The UID :return: Fully qualified topic :rtype : str """ return "/".join( (TOPIC_PREFIX, self.__peer.app_id, UID_TOPIC, subtopic)) def __make_group_topic(self, subtopic): """ Constructs a complete group topic. :param subtopic: The group name :return: Fully qualified topic :rtype : str """ return "/".join( (TOPIC_PREFIX, self.__peer.app_id, GROUP_TOPIC, subtopic)) def __handle_will(self, message): if self.__callback_handler and self.__callback_handler.on_peer_down: self.__callback_handler.on_peer_down( message.payload.decode('utf-8')) else: _log.debug("Missing callback for on_peer_down.") def _on_connect(self, *args, **kwargs): """ Handles a connection-established event. :param args: unnamed arguments :param kwargs: named arguments :return: """ _log.info("Connection established.") _log.debug("Subscribing for topic %s.", self.__make_uid_topic(self.__peer.uid)) self.__mqtt.subscribe(self.__make_uid_topic(self.__peer.uid)) self.__mqtt.subscribe(self.__make_group_topic("all")) self.__mqtt.subscribe(self.__WILL_TOPIC) for group in self.__peer.groups: _log.debug("Subscribing for topic %s.", self.__make_group_topic(group)) self.__mqtt.subscribe(self.__make_group_topic(group)) if self.__callback_handler and self.__callback_handler.on_connected: self.__callback_handler.on_connected() else: _log.warning("Missing callback for on_connect.") def _on_disconnect(self, *args, **kwargs): """ Handles a connection-lost event. :param args: unnamed arguments :param kwargs: named arguments :return: """ _log.info("Connection lost.") if self.__callback_handler and self.__callback_handler.on_disconnected: self.__callback_handler.on_disconnected() def _on_message(self, client, data, message): """ Handles an incoming message. :param client: the client instance for this callback :param data: the private user data :param message: an instance of MQTTMessage :type message: paho.mqtt.client.MQTTMessage :return: """ _log.info("Message received.") if message.topic == self.__WILL_TOPIC: self.__handle_will(message) return if self.__callback_handler and self.__callback_handler.on_message: self.__callback_handler.on_message(message.payload.decode('utf-8')) else: _log.warning("Missing callback for on_message.") def fire(self, peer_uid, message): """ Sends a message to another peer. :param peer_uid: Peer UID :param message: Message content :return: """ self.__mqtt.publish( self.__make_uid_topic(peer_uid), message, 1 ) def fire_group(self, group, message): """ Sends a message to a group of peers. :param group: Group's name :param message: Message content :return: """ self.__mqtt.publish( self.__make_group_topic(group), message, 1 ) def set_callback_listener(self, listener): """ Sets callback listener. :param listener: the listener :return: """ self.__callback_handler = listener def login(self, username, password): """ Set credentials for an MQTT broker. :param username: Username :param password: Password :return: """ self.__mqtt.username_pw_set(username, password) def connect(self, host, port): """ Connects to an MQTT broker. :param host: broker's host name :param port: broker's port number :return: """ _log.info("Connecting to MQTT broker at %s:%s ...", host, port) self.__mqtt.will_set(self.__WILL_TOPIC, self.__peer.uid, 1) self.__mqtt.connect(host, port) self.__mqtt.loop_start() def disconnect(self): """ Diconnects from an MQTT broker. :return: """ _log.info("Disconnecting from MQTT broker...") self.__mqtt.publish(self.__WILL_TOPIC, self.__peer.uid, 1) self.__mqtt.loop_stop() self.__mqtt.disconnect()
def on_mqtt_connect(self, client: mqtt.Client, *args, **kwargs): will_topic = f'{self.topic_prefix}/connected' client.will_set(will_topic, 0, qos=1, retain=True) client.publish(will_topic, 1, qos=1, retain=True) client.subscribe(f"{self.topic_prefix}/update/#") logger.info("MQTT Connected.")
class MQTTConnection(): client: Client _instance = None @classmethod def get_instance(cls) -> "MQTTConnection": if cls._instance is None: cls._instance = MQTTConnection() return cls._instance def __init__(self): self.client = Client( "pai" + os.urandom(8).hex(), protocol=protocol_map.get(str(cfg.MQTT_PROTOCOL), MQTTv311), transport=cfg.MQTT_TRANSPORT, ) self._last_pai_status = "unknown" self.pai_status_topic = "{}/{}/{}".format(cfg.MQTT_BASE_TOPIC, cfg.MQTT_INTERFACE_TOPIC, "pai_status") self.availability_topic = "{}/{}/{}".format(cfg.MQTT_BASE_TOPIC, cfg.MQTT_INTERFACE_TOPIC, "availability") self.client.on_connect = self._on_connect_cb self.client.on_disconnect = self._on_disconnect_cb self.state = ConnectionState.NEW # self.client.enable_logger(logger) # self.client.on_subscribe = lambda client, userdata, mid, granted_qos: logger.debug("Subscribed: %s" %(mid)) # self.client.on_message = lambda client, userdata, message: logger.debug("Message received: %s" % str(message)) # self.client.on_publish = lambda client, userdata, mid: logger.debug("Message published: %s" % str(mid)) ps.subscribe(self.on_run_state_change, "run-state") self.registrars = [] if cfg.MQTT_USERNAME is not None and cfg.MQTT_PASSWORD is not None: self.client.username_pw_set(username=cfg.MQTT_USERNAME, password=cfg.MQTT_PASSWORD) if cfg.MQTT_TLS_CERT_PATH is not None: self.client.tls_set( ca_certs=cfg.MQTT_TLS_CERT_PATH, certfile=None, keyfile=None, cert_reqs=ssl.CERT_REQUIRED, tls_version=ssl.PROTOCOL_TLSv1_2, ciphers=None, ) self.client.tls_insecure_set(False) self.client.will_set(self.availability_topic, "offline", 0, retain=True) self.client.on_log = self.on_client_log def on_client_log(self, client, userdata, level, buf): level_std = LOGGING_LEVEL[level] exc_info = None type_, exc, trace = sys.exc_info() if exc: # Can be (socket.error, OSError, WebsocketConnectionError, ...) if hasattr(exc, "errno"): exc_msg = f"{os.strerror(exc.errno)}({exc.errno})" if exc.errno in [22, 49]: level_std = logging.ERROR buf = f"{buf}: Please check MQTT connection settings. Especially MQTT_BIND_ADDRESS and MQTT_BIND_PORT" else: exc_msg = str(exc) buf = f"{buf}: {exc_msg}" if "Connection failed" in buf: level_std = logging.WARNING if level_std > logging.DEBUG: logger.log(level_std, buf, exc_info=exc_info) def on_run_state_change(self, state: RunState): v = RUN_STATE_2_PAYLOAD.get(state, "unknown") self._report_pai_status(v) def start(self): if self.state == ConnectionState.NEW: self.client.loop_start() # TODO: Some initial connection retry mechanism required try: self.client.connect_async( host=cfg.MQTT_HOST, port=cfg.MQTT_PORT, keepalive=cfg.MQTT_KEEPALIVE, bind_address=cfg.MQTT_BIND_ADDRESS, bind_port=cfg.MQTT_BIND_PORT, ) self.state = ConnectionState.CONNECTING logger.info("MQTT loop started") except socket.gaierror: logger.exception("Failed to connect to MQTT (%s:%d)", cfg.MQTT_HOST, cfg.MQTT_PORT) def stop(self): if self.state in [ ConnectionState.CONNECTING, ConnectionState.CONNECTED ]: self.disconnect() self.client.loop_stop() logger.info("MQTT loop stopped") def publish(self, topic, payload=None, *args, **kwargs): logger.debug("MQTT: {}={}".format(topic, payload)) self.client.publish(topic, payload, *args, **kwargs) def _call_registars(self, method, *args, **kwargs): for r in self.registrars: try: if hasattr(r, method) and isinstance(getattr(r, method), typing.Callable): getattr(r, method)(*args, **kwargs) except: logger.exception('Failed to call "%s" on "%s"', method, r.__class__.__name__) def register(self, cls): self.registrars.append(cls) self.start() def unregister(self, cls): self.registrars.remove(cls) if len(self.registrars) == 0: self.stop() @property def connected(self): return self.state == ConnectionState.CONNECTED def _report_pai_status(self, status): self._last_pai_status = status self.publish(self.pai_status_topic, status, qos=cfg.MQTT_QOS, retain=True) self.publish( self.availability_topic, "online" if status in ["online", "paused"] else "offline", qos=cfg.MQTT_QOS, retain=True, ) def _on_connect_cb(self, client, userdata, flags, result, properties=None): # called on Thread-6 if result == MQTT_ERR_SUCCESS: logger.info("MQTT Broker Connected") self.state = ConnectionState.CONNECTED self._report_pai_status(self._last_pai_status) self._call_registars("on_connect", client, userdata, flags, result) else: logger.error( f"Failed to connect to MQTT: {connack_string(result)} ({result})" ) def _on_disconnect_cb(self, client, userdata, rc): # called on Thread-6 if rc == MQTT_ERR_SUCCESS: logger.info("MQTT Broker Disconnected") else: logger.error(f"MQTT Broker unexpectedly disconnected. Code: {rc}") self.state = ConnectionState.NEW self._call_registars("on_disconnect", client, userdata, rc) def disconnect(self, reasoncode=None, properties=None): self.state = ConnectionState.DISCONNECTING self._report_pai_status("offline") self.client.disconnect() def message_callback_add(self, *args, **kwargs): self.client.message_callback_add(*args, **kwargs) def subscribe(self, *args, **kwargs): self.client.subscribe(*args, **kwargs)
class LampiApp(App): _updated = False _updatingUI = False _hue = NumericProperty() _saturation = NumericProperty() _brightness = NumericProperty() lamp_is_on = BooleanProperty() def _get_hue(self): return self._hue def _set_hue(self, value): self._hue = value def _get_saturation(self): return self._saturation def _set_saturation(self, value): self._saturation = value def _get_brightness(self): return self._brightness def _set_brightness(self, value): self._brightness = value hue = AliasProperty(_get_hue, _set_hue, bind=['_hue']) saturation = AliasProperty(_get_saturation, _set_saturation, bind=['_saturation']) brightness = AliasProperty(_get_brightness, _set_brightness, bind=['_brightness']) gpio17_pressed = BooleanProperty(False) device_associated = BooleanProperty(True) def on_start(self): self.keen = KeenEventRecorder(PROJECT_ID, WRITE_KEY, get_device_id()) self._publish_clock = None self.mqtt_broker_bridged = False self._associated = True self.association_code = None self.mqtt = Client(client_id=MQTT_CLIENT_ID) self.mqtt.will_set(client_state_topic(MQTT_CLIENT_ID), "0", qos=2, retain=True) self.mqtt.on_connect = self.on_connect self.mqtt.connect(MQTT_BROKER_HOST, port=MQTT_BROKER_PORT, keepalive=MQTT_BROKER_KEEP_ALIVE_SECS) self.mqtt.loop_start() self.set_up_GPIO_and_device_status_popup() self.associated_status_popup = self._build_associated_status_popup() self.associated_status_popup.bind(on_open=self.update_popup_associated) Clock.schedule_interval(self._poll_associated, 0.1) def _build_associated_status_popup(self): return Popup(title='Associate your Lamp', content=Label(text='Msg here', font_size='30sp'), size_hint=(1, 1), auto_dismiss=False) def on_hue(self, instance, value): if self._updatingUI: return evt = self._create_event_record('hue-slider', value) self.keen.record_event('ui', evt) if self._publish_clock is None: self._publish_clock = Clock.schedule_once( lambda dt: self._update_leds(), 0.01) def on_saturation(self, instance, value): if self._updatingUI: return evt = self._create_event_record('saturation-slider', value) self.keen.record_event('ui', evt) if self._publish_clock is None: self._publish_clock = Clock.schedule_once( lambda dt: self._update_leds(), 0.01) def on_brightness(self, instance, value): if self._updatingUI: return evt = self._create_event_record('brightness-slider', value) self.keen.record_event('ui', evt) if self._publish_clock is None: self._publish_clock = Clock.schedule_once( lambda dt: self._update_leds(), 0.01) def on_lamp_is_on(self, instance, value): if self._updatingUI: return evt = self._create_event_record('power', value) self.keen.record_event('ui', evt) if self._publish_clock is None: self._publish_clock = Clock.schedule_once( lambda dt: self._update_leds(), 0.01) def _create_event_record(self, element, value): return {'element': {'id': element, 'value': value}} def on_connect(self, client, userdata, flags, rc): self.mqtt.publish(client_state_topic(MQTT_CLIENT_ID), "1", qos=2, retain=True) self.mqtt.message_callback_add(TOPIC_LAMP_CHANGE_NOTIFICATION, self.receive_new_lamp_state) self.mqtt.message_callback_add(broker_bridge_connection_topic(), self.receive_bridge_connection_status) self.mqtt.message_callback_add(TOPIC_LAMP_ASSOCIATED, self.receive_associated) self.mqtt.subscribe(broker_bridge_connection_topic(), qos=1) self.mqtt.subscribe(TOPIC_LAMP_CHANGE_NOTIFICATION, qos=1) self.mqtt.subscribe(TOPIC_LAMP_ASSOCIATED, qos=2) def _poll_associated(self, dt): # this polling loop allows us to synchronize changes from the # MQTT callbacks (which happen in a different thread) to the # Kivy UI self.device_associated = self._associated def receive_associated(self, client, userdata, message): # this is called in MQTT event loop thread new_associated = json.loads(message.payload) if self._associated != new_associated['associated']: if not new_associated['associated']: self.association_code = new_associated['code'] else: self.association_code = None self._associated = new_associated['associated'] def on_device_associated(self, instance, value): if value: self.associated_status_popup.dismiss() else: self.associated_status_popup.open() def update_popup_associated(self, instance): code = self.association_code[0:6] instance.content.text = ("Please use the\n" "following code\n" "to associate\n" "your device\n" "on the Web\n{}".format(code)) def receive_bridge_connection_status(self, client, userdata, message): # monitor if the MQTT bridge to our cloud broker is up if message.payload == "1": self.mqtt_broker_bridged = True else: self.mqtt_broker_bridged = False def receive_new_lamp_state(self, client, userdata, message): new_state = json.loads(message.payload) Clock.schedule_once(lambda dt: self._update_ui(new_state), 0.01) def _update_ui(self, new_state): if self._updated and new_state['client'] == MQTT_CLIENT_ID: # ignore updates generated by this client, except the first to # make sure the UI is syncrhonized with the lamp_service return self._updatingUI = True try: if 'color' in new_state: self.hue = new_state['color']['h'] self.saturation = new_state['color']['s'] if 'brightness' in new_state: self.brightness = new_state['brightness'] if 'on' in new_state: self.lamp_is_on = new_state['on'] finally: self._updatingUI = False self._updated = True def _update_leds(self): msg = { 'color': { 'h': self._hue, 's': self._saturation }, 'brightness': self._brightness, 'on': self.lamp_is_on, 'client': MQTT_CLIENT_ID } self.mqtt.publish(TOPIC_SET_LAMP_CONFIG, json.dumps(msg), qos=1) self._publish_clock = None def set_up_GPIO_and_device_status_popup(self): self.pi = pigpio.pi() self.pi.set_mode(17, pigpio.INPUT) self.pi.set_pull_up_down(17, pigpio.PUD_UP) Clock.schedule_interval(self._poll_GPIO, 0.05) self.network_status_popup = self._build_network_status_popup() self.network_status_popup.bind(on_open=self.update_device_status_popup) def _build_network_status_popup(self): return Popup(title='Device Status', content=Label(text='IP ADDRESS WILL GO HERE'), size_hint=(1, 1), auto_dismiss=False) def update_device_status_popup(self, instance): interface = "wlan0" ipaddr = lampi_util.get_ip_address(interface) deviceid = lampi_util.get_device_id() msg = ("Version: {}\n" "{}: {}\n" "DeviceID: {}\n" "Broker Bridged: {}\n" "threaded").format(LAMPI_APP_VERSION, interface, ipaddr, deviceid, self.mqtt_broker_bridged) instance.content.text = msg def on_gpio17_pressed(self, instance, value): if value: self.network_status_popup.open() else: self.network_status_popup.dismiss() def _poll_GPIO(self, dt): # GPIO17 is the rightmost button when looking front of LAMPI self.gpio17_pressed = not self.pi.read(17)
class Translator(object): """Translates messages between the LifeSOS and MQTT interfaces.""" # Default interval to wait before resetting Trigger device state to Off AUTO_RESET_INTERVAL = 30 # Keys for Home Assistant MQTT discovery configuration HA_AVAILABILITY_TOPIC = 'availability_topic' HA_COMMAND_TOPIC = 'command_topic' HA_DEVICE_CLASS = 'device_class' HA_ICON = 'icon' HA_NAME = 'name' HA_PAYLOAD_ARM_AWAY = 'payload_arm_away' HA_PAYLOAD_ARM_HOME = 'payload_arm_home' HA_PAYLOAD_AVAILABLE = 'payload_available' HA_PAYLOAD_DISARM = 'payload_disarm' HA_PAYLOAD_NOT_AVAILABLE = 'payload_not_available' HA_PAYLOAD_OFF = 'payload_off' HA_PAYLOAD_ON = 'payload_on' HA_STATE_TOPIC = 'state_topic' HA_UNIQUE_ID = 'unique_id' HA_UNIT_OF_MEASUREMENT = 'unit_of_measurement' # Device class to classify the sensor type in Home Assistant HA_DC_DOOR = 'door' HA_DC_GAS = 'gas' HA_DC_HUMIDITY = 'humidity' HA_DC_ILLUMINANCE = 'illuminance' HA_DC_MOISTURE = 'moisture' HA_DC_MOTION = 'motion' HA_DC_SAFETY = 'safety' HA_DC_SMOKE = 'smoke' HA_DC_TEMPERATURE = 'temperature' HA_DC_VIBRATION = 'vibration' HA_DC_WINDOW = 'window' HA_DC_BATTERY = 'battery' # Icons in Home Assistant HA_ICON_RSSI = 'mdi:wifi' # Platforms in Home Assistant to represent our devices HA_PLATFORM_ALARM_CONTROL_PANEL = 'alarm_control_panel' HA_PLATFORM_BINARY_SENSOR = 'binary_sensor' HA_PLATFORM_SENSOR = 'sensor' HA_PLATFORM_SWITCH = 'switch' # Alarm states in Home Assistant HA_STATE_ARMED_AWAY = 'armed_away' HA_STATE_ARMED_HOME = 'armed_home' HA_STATE_DISARMED = 'disarmed' HA_STATE_PENDING = 'pending' HA_STATE_TRIGGERED = 'triggered' # Unit of measurement for Home Assistant sensors HA_UOM_CURRENT = 'A' HA_UOM_HUMIDITY = '%' HA_UOM_ILLUMINANCE = 'Lux' HA_UOM_RSSI = 'dB' HA_UOM_TEMPERATURE = '°C' # Ping MQTT broker this many seconds apart to check we're connected KEEP_ALIVE = 30 # Attempt reconnection this many seconds apart # (starts at min, doubles on retry until max reached) RECONNECT_MAX_DELAY = 120 RECONNECT_MIN_DELAY = 15 # Sub-topic to clear the alarm/warning LEDs on base unit and stop siren TOPIC_CLEAR_STATUS = 'clear_status' # Sub-topic to access the remote date/time TOPIC_DATETIME = 'datetime' # Sub-topic to provide alarm state that is recognised by Home Assistant TOPIC_HASTATE = 'ha_state' # Sub-topic that will be subscribed to on topics that can be set TOPIC_SET = 'set' def __init__(self, config: Config): self._config = config self._loop = asyncio.get_event_loop() self._shutdown = False self._get_task = None self._auto_reset_handles = {} self._state = None self._ha_state = None # Create LifeSOS base unit instance and attach callbacks self._baseunit = BaseUnit(self._config.lifesos.host, self._config.lifesos.port) if self._config.lifesos.password: self._baseunit.password = self._config.lifesos.password self._baseunit.on_device_added = self._baseunit_device_added self._baseunit.on_device_deleted = self._baseunit_device_deleted self._baseunit.on_event = self._baseunit_event self._baseunit.on_properties_changed = self._baseunit_properties_changed self._baseunit.on_switch_state_changed = self._baseunit_switch_state_changed # Create MQTT client instance self._mqtt = MQTTClient(client_id=self._config.mqtt.client_id, clean_session=False) self._mqtt.enable_logger() self._mqtt.will_set( '{}/{}'.format(self._config.translator.baseunit.topic, BaseUnit.PROP_IS_CONNECTED), str(False).encode(), QOS_1, True) self._mqtt.reconnect_delay_set(Translator.RECONNECT_MIN_DELAY, Translator.RECONNECT_MAX_DELAY) if self._config.mqtt.uri.username: self._mqtt.username_pw_set(self._config.mqtt.uri.username, self._config.mqtt.uri.password) if self._config.mqtt.uri.scheme == SCHEME_MQTTS: self._mqtt.tls_set() self._mqtt.on_connect = self._mqtt_on_connect self._mqtt.on_disconnect = self._mqtt_on_disconnect self._mqtt.on_message = self._mqtt_on_message self._mqtt_was_connected = False self._mqtt_last_connection = None self._mqtt_last_disconnection = None # Generate a list of topics we'll need to subscribe to self._subscribetopics = [] self._subscribetopics.append( SubscribeTopic( '{}/{}'.format(self._config.translator.baseunit.topic, Translator.TOPIC_CLEAR_STATUS), self._on_message_clear_status)) self._subscribetopics.append( SubscribeTopic( '{}/{}/{}'.format(self._config.translator.baseunit.topic, Translator.TOPIC_DATETIME, Translator.TOPIC_SET), self._on_message_set_datetime)) names = [BaseUnit.PROP_OPERATION_MODE] for name in names: self._subscribetopics.append( SubscribeTopic('{}/{}/{}'.format( self._config.translator.baseunit.topic, name, Translator.TOPIC_SET), self._on_message_baseunit, args=name)) for switch_number in self._config.translator.switches.keys(): switch_config = self._config.translator.switches.get(switch_number) if switch_config and switch_config.topic: self._subscribetopics.append( SubscribeTopic('{}/{}'.format(switch_config.topic, Translator.TOPIC_SET), self._on_message_switch, args=switch_number)) if self._config.translator.ha_birth_topic: self._subscribetopics.append( SubscribeTopic(self._config.translator.ha_birth_topic, self._on_ha_message)) # Also create a lookup dict for the topics to subscribe to self._subscribetopics_lookup = \ {st.topic: st for st in self._subscribetopics} # Create queue to store pending messages from our subscribed topics self._pending_messages = Queue() # # METHODS - Public # async def async_start(self) -> None: """Starts up the LifeSOS interface and connects to MQTT broker.""" self._shutdown = False # Start up the LifeSOS interface self._baseunit.start() # Connect to the MQTT broker self._mqtt_was_connected = False if self._config.mqtt.uri.port: self._mqtt.connect_async(self._config.mqtt.uri.hostname, self._config.mqtt.uri.port, keepalive=Translator.KEEP_ALIVE) else: self._mqtt.connect_async(self._config.mqtt.uri.hostname, keepalive=Translator.KEEP_ALIVE) # Start processing MQTT messages self._mqtt.loop_start() async def async_loop(self) -> None: """Loop indefinitely to process messages from our subscriptions.""" # Trap SIGINT and SIGTERM so that we can shutdown gracefully signal.signal(signal.SIGINT, self.signal_shutdown) signal.signal(signal.SIGTERM, self.signal_shutdown) try: while not self._shutdown: # Wait for next message self._get_task = self._loop.create_task( self._pending_messages.async_q.get()) try: message = await self._get_task except asyncio.CancelledError: _LOGGER.debug('Translator loop cancelled.') continue except Exception: # pylint: disable=broad-except # Log any exception but keep going _LOGGER.error( "Exception waiting for message to be delivered", exc_info=True) continue finally: self._get_task = None # Do subscribed topic callback to handle message try: subscribetopic = self._subscribetopics_lookup[ message.topic] subscribetopic.on_message(subscribetopic, message) except Exception: # pylint: disable=broad-except _LOGGER.error( "Exception processing message from subscribed topic: %s", message.topic, exc_info=True) finally: self._pending_messages.async_q.task_done() # Turn off is_connected flag before leaving self._publish_baseunit_property(BaseUnit.PROP_IS_CONNECTED, False) await asyncio.sleep(0) finally: signal.signal(signal.SIGINT, signal.SIG_DFL) async def async_stop(self) -> None: """Shuts down the LifeSOS interface and disconnects from MQTT broker.""" # Stop the LifeSOS interface self._baseunit.stop() # Cancel any outstanding auto reset tasks for item in self._auto_reset_handles.copy().items(): item[1].cancel() self._auto_reset_handles.pop(item[0]) # Stop processing MQTT messages self._mqtt.loop_stop() # Disconnect from the MQTT broker self._mqtt.disconnect() def signal_shutdown(self, sig, frame): """Flag shutdown when signal received.""" _LOGGER.debug('%s received; shutting down...', signal.Signals(sig).name) # pylint: disable=no-member self._shutdown = True if self._get_task: self._get_task.cancel() # Issue #8 - Cancel not processed until next message added to queue. # Just put a dummy object on the queue to ensure it is handled immediately. self._pending_messages.sync_q.put_nowait(None) # # METHODS - Private / Internal # def _mqtt_on_connect(self, client: MQTTClient, userdata: Any, flags: Dict[str, Any], result_code: int) -> None: # On error, log it and don't go any further; client will retry if result_code != CONNACK_ACCEPTED: _LOGGER.warning(connack_string(result_code)) # pylint: disable=no-member return # Successfully connected self._mqtt_last_connection = datetime.now() if not self._mqtt_was_connected: _LOGGER.debug("MQTT client connected to broker") self._mqtt_was_connected = True else: try: outage = self._mqtt_last_connection - self._mqtt_last_disconnection _LOGGER.warning( "MQTT client reconnected to broker. " "Outage duration was %s", str(outage)) except Exception: # pylint: disable=broad-except _LOGGER.warning("MQTT client reconnected to broker") # Republish the 'is_connected' state; this will have automatically # been set to False on MQTT client disconnection due to our will # (even though this app might still be connected to the LifeSOS unit) self._publish( '{}/{}'.format(self._config.translator.baseunit.topic, BaseUnit.PROP_IS_CONNECTED), self._baseunit.is_connected, True) # Subscribe to topics we are capable of actioning for subscribetopic in self._subscribetopics: self._mqtt.subscribe(subscribetopic.topic, subscribetopic.qos) def _mqtt_on_disconnect(self, client: MQTTClient, userdata: Any, result_code: int) -> None: # When disconnected from broker and we didn't initiate it... if result_code != MQTT_ERR_SUCCESS: _LOGGER.warning( "MQTT client lost connection to broker (RC: %i). " "Will attempt to reconnect periodically", result_code) self._mqtt_last_disconnection = datetime.now() def _mqtt_on_message(self, client: MQTTClient, userdata: Any, message: MQTTMessage): # Add message to our queue, to be processed on main thread self._pending_messages.sync_q.put_nowait(message) def _baseunit_device_added(self, baseunit: BaseUnit, device: Device) -> None: # Hook up callbacks for device that was added / discovered device.on_event = self._device_on_event device.on_properties_changed = self._device_on_properties_changed # Get configuration settings for device; don't go any further when # device is not included in the config device_config = self._config.translator.devices.get(device.device_id) if not device_config: _LOGGER.warning( "Ignoring device as it was not listed in the config file: %s", device) return # Publish initial property values for device if device_config.topic: props = device.as_dict() for name in props.keys(): self._publish_device_property(device_config.topic, device, name, getattr(device, name)) # When HA discovery is enabled, publish device configuration to it if self._config.translator.ha_discovery_prefix: if device_config.ha_name: self._publish_ha_device_config(device, device_config) if device_config.ha_name_rssi: self._publish_ha_device_rssi_config(device, device_config) if device_config.ha_name_battery: self._publish_ha_device_battery_config(device, device_config) def _baseunit_device_deleted( self, baseunit: BaseUnit, device: Device) -> None: # pylint: disable=no-self-use # Remove callbacks from deleted device device.on_event = None device.on_properties_changed = None def _baseunit_event(self, baseunit: BaseUnit, contact_id: ContactID): # When base unit event occurs, publish the event data # (don't bother retaining; events are time sensitive) event_data = json.dumps(contact_id.as_dict()) self._publish( '{}/event'.format(self._config.translator.baseunit.topic), event_data, False) # For clients that can't handle json, we will also provide the event # qualifier and code via these topics if contact_id.event_code: if contact_id.event_qualifier == EventQualifier.Event: self._publish( '{}/event_code'.format( self._config.translator.baseunit.topic), contact_id.event_code, False) elif contact_id.event_qualifier == EventQualifier.Restore: self._publish( '{}/restore_code'.format( self._config.translator.baseunit.topic), contact_id.event_code, False) # This is just for Home Assistant; the 'alarm_control_panel.mqtt' # component currently requires these hard-coded state values if contact_id.event_qualifier == EventQualifier.Event and \ contact_id.event_category == EventCategory.Alarm: self._ha_state = Translator.HA_STATE_TRIGGERED self._publish( '{}/{}'.format(self._config.translator.baseunit.topic, Translator.TOPIC_HASTATE), self._ha_state, True) def _baseunit_properties_changed( self, baseunit: BaseUnit, changes: List[PropertyChangedInfo]) -> None: # When base unit properties change, publish them has_connected = False for change in changes: self._publish_baseunit_property(change.name, change.new_value) # Also check if connection has just been established if change.name == BaseUnit.PROP_IS_CONNECTED and change.new_value: has_connected = True # On connection, publish config for Home Assistant if needed if has_connected: self._publish_ha_config() def _baseunit_switch_state_changed(self, baseunit: BaseUnit, switch_number: SwitchNumber, state: Optional[bool]) -> None: # When switch state changes, publish it switch_config = self._config.translator.switches.get(switch_number) if switch_config and switch_config.topic: self._publish(switch_config.topic, OnOff.parse_value(state), True) def _device_on_event(self, device: Device, event_code: DeviceEventCode) -> None: device_config = self._config.translator.devices.get(device.device_id) if device_config and device_config.topic: # When device event occurs, publish the event code # (don't bother retaining; events are time sensitive) self._publish('{}/event_code'.format(device_config.topic), event_code, False) # When it is a Trigger event, set state to On and schedule an # auto reset callback to occur after specified interval if event_code == DeviceEventCode.Trigger: self._publish(device_config.topic, OnOff.parse_value(True), True) handle = self._auto_reset_handles.get(device.device_id) if handle: handle.cancel() handle = self._loop.call_later( device_config.auto_reset_interval or Translator.AUTO_RESET_INTERVAL, self._auto_reset, device.device_id) self._auto_reset_handles[device.device_id] = handle def _auto_reset(self, device_id: int): # Auto reset a Trigger device to Off state device_config = self._config.translator.devices.get(device_id) if device_config and device_config.topic: self._publish(device_config.topic, OnOff.parse_value(False), True) self._auto_reset_handles.pop(device_id) def _device_on_properties_changed(self, device: Device, changes: List[PropertyChangedInfo]): # When device properties change, publish them device_config = self._config.translator.devices.get(device.device_id) if device_config and device_config.topic: for change in changes: self._publish_device_property(device_config.topic, device, change.name, change.new_value) def _publish_baseunit_property(self, name: str, value: Any) -> None: topic_parent = self._config.translator.baseunit.topic # Base Unit topic holds the state if name == BaseUnit.PROP_STATE: self._state = value self._publish(topic_parent, value, True) # This is just for Home Assistant; the 'alarm_control_panel.mqtt' # component currently requires these hard-coded state values topic = '{}/{}'.format(topic_parent, Translator.TOPIC_HASTATE) if value in {BaseUnitState.Disarm, BaseUnitState.Monitor}: self._ha_state = Translator.HA_STATE_DISARMED self._publish(topic, self._ha_state, True) elif value == BaseUnitState.Home: self._ha_state = Translator.HA_STATE_ARMED_HOME self._publish(topic, self._ha_state, True) elif value == BaseUnitState.Away: self._ha_state = Translator.HA_STATE_ARMED_AWAY self._publish(topic, self._ha_state, True) elif value in { BaseUnitState.AwayExitDelay, BaseUnitState.AwayEntryDelay }: self._ha_state = Translator.HA_STATE_PENDING self._publish(topic, self._ha_state, True) # Other supported properties in a topic using property name elif name in { BaseUnit.PROP_IS_CONNECTED, BaseUnit.PROP_ROM_VERSION, BaseUnit.PROP_EXIT_DELAY, BaseUnit.PROP_ENTRY_DELAY, BaseUnit.PROP_OPERATION_MODE }: self._publish('{}/{}'.format(topic_parent, name), value, True) def _publish_device_property(self, topic_parent: str, device: Device, name: str, value: Any) -> None: # Device topic holds the state if (not isinstance(device, SpecialDevice)) and \ name == Device.PROP_IS_CLOSED: # For regular device; this is the Is Closed property for magnet # sensors, otherwise default to Off for trigger-based devices if device.type == DeviceType.DoorMagnet: self._publish(topic_parent, OpenClosed.parse_value(value), True) else: self._publish(topic_parent, OnOff.Off, True) elif isinstance(device, SpecialDevice) and \ name == SpecialDevice.PROP_CURRENT_READING: # For special device, this is the current reading self._publish(topic_parent, value, True) # Category will have sub-topics for it's properties elif name == Device.PROP_CATEGORY: for prop in value.as_dict().items(): if prop[0] in {'code', 'description'}: self._publish( '{}/{}/{}'.format(topic_parent, name, prop[0]), prop[1], True) # Flag enums; expose as sub-topics with a bool state per flag elif name == Device.PROP_CHARACTERISTICS: for item in iter(DCFlags): self._publish('{}/{}/{}'.format(topic_parent, name, item.name), bool(value & item.value), True) elif name == Device.PROP_ENABLE_STATUS: for item in iter(ESFlags): self._publish('{}/{}/{}'.format(topic_parent, name, item.name), bool(value & item.value), True) elif name == Device.PROP_SWITCHES: for item in iter(SwitchFlags): self._publish('{}/{}/{}'.format(topic_parent, name, item.name), bool(value & item.value), True) elif name == SpecialDevice.PROP_SPECIAL_STATUS: for item in iter(SSFlags): self._publish('{}/{}/{}'.format(topic_parent, name, item.name), bool(value & item.value), True) # Device ID; value should be formatted as hex elif name == Device.PROP_DEVICE_ID: self._publish('{}/{}'.format(topic_parent, name), '{:06x}'.format(value), True) # Other supported properties in a topic using property name elif name in { Device.PROP_DEVICE_ID, Device.PROP_ZONE, Device.PROP_TYPE, Device.PROP_RSSI_DB, Device.PROP_RSSI_BARS, SpecialDevice.PROP_HIGH_LIMIT, SpecialDevice.PROP_LOW_LIMIT, SpecialDevice.PROP_CONTROL_LIMIT_FIELDS_EXIST, SpecialDevice.PROP_CONTROL_HIGH_LIMIT, SpecialDevice.PROP_CONTROL_LOW_LIMIT }: self._publish('{}/{}'.format(topic_parent, name), value, True) def _publish_ha_config(self): # Skip if Home Assistant discovery disabled if not self._config.translator.ha_discovery_prefix: return # Publish config for the base unit when enabled if self._config.translator.baseunit.ha_name: self._publish_ha_baseunit_config(self._config.translator.baseunit) # Publish config for each device when enabled for device_id in self._config.translator.devices.keys(): if self._shutdown: return device_config = self._config.translator.devices[device_id] device = self._baseunit.devices.get(device_id) if device: if device_config.ha_name: self._publish_ha_device_config(device, device_config) if device_config.ha_name_rssi: self._publish_ha_device_rssi_config(device, device_config) if device_config.ha_name_battery: self._publish_ha_device_battery_config( device, device_config) # Publish config for each switch when enabled for switch_number in self._config.translator.switches.keys(): if self._shutdown: return switch_config = self._config.translator.switches[switch_number] if switch_config.ha_name: self._publish_ha_switch_config(switch_number, switch_config) def _publish_ha_baseunit_config(self, baseunit_config: TranslatorBaseUnitConfig): # Generate message that can be used to automatically configure the # alarm control panel in Home Assistant using MQTT Discovery message = { Translator.HA_NAME: baseunit_config.ha_name, Translator.HA_UNIQUE_ID: '{}'.format(PROJECT_NAME), Translator.HA_STATE_TOPIC: '{}/{}'.format(baseunit_config.topic, Translator.TOPIC_HASTATE), Translator.HA_COMMAND_TOPIC: '{}/{}/{}'.format(baseunit_config.topic, BaseUnit.PROP_OPERATION_MODE, Translator.TOPIC_SET), Translator.HA_PAYLOAD_DISARM: str(OperationMode.Disarm), Translator.HA_PAYLOAD_ARM_HOME: str(OperationMode.Home), Translator.HA_PAYLOAD_ARM_AWAY: str(OperationMode.Away), Translator.HA_AVAILABILITY_TOPIC: '{}/{}'.format(baseunit_config.topic, BaseUnit.PROP_IS_CONNECTED), Translator.HA_PAYLOAD_AVAILABLE: str(True), Translator.HA_PAYLOAD_NOT_AVAILABLE: str(False), } self._publish( '{}/{}/{}/config'.format( self._config.translator.ha_discovery_prefix, Translator.HA_PLATFORM_ALARM_CONTROL_PANEL, message[Translator.HA_UNIQUE_ID]), json.dumps(message), False) def _publish_ha_device_config(self, device: Device, device_config: TranslatorDeviceConfig): # Generate message that can be used to automatically configure the # device in Home Assistant using MQTT Discovery message = { Translator.HA_NAME: device_config.ha_name, Translator.HA_UNIQUE_ID: '{}_{:06x}'.format(PROJECT_NAME, device.device_id), Translator.HA_STATE_TOPIC: device_config.topic, Translator.HA_AVAILABILITY_TOPIC: '{}/{}'.format(self._config.translator.baseunit.topic, BaseUnit.PROP_IS_CONNECTED), Translator.HA_PAYLOAD_AVAILABLE: str(True), Translator.HA_PAYLOAD_NOT_AVAILABLE: str(False), } if device.type in { DeviceType.FloodDetector, DeviceType.FloodDetector2 }: ha_platform = Translator.HA_PLATFORM_BINARY_SENSOR message[Translator.HA_DEVICE_CLASS] = Translator.HA_DC_MOISTURE message[Translator.HA_PAYLOAD_ON] = str(OnOff.On) message[Translator.HA_PAYLOAD_OFF] = str(OnOff.Off) elif device.type in {DeviceType.MedicalButton}: ha_platform = Translator.HA_PLATFORM_BINARY_SENSOR message[Translator.HA_DEVICE_CLASS] = Translator.HA_DC_SAFETY message[Translator.HA_PAYLOAD_ON] = str(OnOff.On) message[Translator.HA_PAYLOAD_OFF] = str(OnOff.Off) elif device.type in { DeviceType.AnalogSensor, DeviceType.AnalogSensor2 }: ha_platform = Translator.HA_PLATFORM_BINARY_SENSOR message[Translator.HA_PAYLOAD_ON] = str(OnOff.On) message[Translator.HA_PAYLOAD_OFF] = str(OnOff.Off) elif device.type in {DeviceType.SmokeDetector}: ha_platform = Translator.HA_PLATFORM_BINARY_SENSOR message[Translator.HA_DEVICE_CLASS] = Translator.HA_DC_SMOKE message[Translator.HA_PAYLOAD_ON] = str(OnOff.On) message[Translator.HA_PAYLOAD_OFF] = str(OnOff.Off) elif device.type in { DeviceType.PressureSensor, DeviceType.PressureSensor2 }: ha_platform = Translator.HA_PLATFORM_BINARY_SENSOR message[Translator.HA_DEVICE_CLASS] = Translator.HA_DC_MOTION message[Translator.HA_PAYLOAD_ON] = str(OnOff.On) message[Translator.HA_PAYLOAD_OFF] = str(OnOff.Off) elif device.type in { DeviceType.CODetector, DeviceType.CO2Sensor, DeviceType.CO2Sensor2, DeviceType.GasDetector }: ha_platform = Translator.HA_PLATFORM_BINARY_SENSOR message[Translator.HA_DEVICE_CLASS] = Translator.HA_DC_GAS message[Translator.HA_PAYLOAD_ON] = str(OnOff.On) message[Translator.HA_PAYLOAD_OFF] = str(OnOff.Off) elif device.type in {DeviceType.DoorMagnet}: ha_platform = Translator.HA_PLATFORM_BINARY_SENSOR message[Translator.HA_DEVICE_CLASS] = Translator.HA_DC_DOOR message[Translator.HA_PAYLOAD_ON] = str(OpenClosed.Open) message[Translator.HA_PAYLOAD_OFF] = str(OpenClosed.Closed) elif device.type in {DeviceType.VibrationSensor}: ha_platform = Translator.HA_PLATFORM_BINARY_SENSOR message[Translator.HA_DEVICE_CLASS] = Translator.HA_DC_VIBRATION message[Translator.HA_PAYLOAD_ON] = str(OnOff.On) message[Translator.HA_PAYLOAD_OFF] = str(OnOff.Off) elif device.type in {DeviceType.PIRSensor}: ha_platform = Translator.HA_PLATFORM_BINARY_SENSOR message[Translator.HA_DEVICE_CLASS] = Translator.HA_DC_MOTION message[Translator.HA_PAYLOAD_ON] = str(OnOff.On) message[Translator.HA_PAYLOAD_OFF] = str(OnOff.Off) elif device.type in {DeviceType.GlassBreakDetector}: ha_platform = Translator.HA_PLATFORM_BINARY_SENSOR message[Translator.HA_DEVICE_CLASS] = Translator.HA_DC_WINDOW message[Translator.HA_PAYLOAD_ON] = str(OnOff.On) message[Translator.HA_PAYLOAD_OFF] = str(OnOff.Off) elif device.type in {DeviceType.HumidSensor, DeviceType.HumidSensor2}: ha_platform = Translator.HA_PLATFORM_SENSOR message[Translator.HA_DEVICE_CLASS] = Translator.HA_DC_HUMIDITY message[ Translator.HA_UNIT_OF_MEASUREMENT] = Translator.HA_UOM_HUMIDITY elif device.type in {DeviceType.TempSensor, DeviceType.TempSensor2}: ha_platform = Translator.HA_PLATFORM_SENSOR message[Translator.HA_DEVICE_CLASS] = Translator.HA_DC_TEMPERATURE message[Translator. HA_UNIT_OF_MEASUREMENT] = Translator.HA_UOM_TEMPERATURE elif device.type in {DeviceType.LightSensor, DeviceType.LightDetector}: ha_platform = Translator.HA_PLATFORM_SENSOR message[Translator.HA_DEVICE_CLASS] = Translator.HA_DC_ILLUMINANCE message[Translator. HA_UNIT_OF_MEASUREMENT] = Translator.HA_UOM_ILLUMINANCE elif device.type in { DeviceType.ACCurrentMeter, DeviceType.ACCurrentMeter2, DeviceType.ThreePhaseACMeter }: ha_platform = Translator.HA_PLATFORM_SENSOR message[ Translator.HA_UNIT_OF_MEASUREMENT] = Translator.HA_UOM_CURRENT else: _LOGGER.warning( "Device type '%s' cannot be represented in Home " "Assistant and will be skipped.", str(device.type)) return self._publish( '{}/{}/{}/config'.format( self._config.translator.ha_discovery_prefix, ha_platform, message[Translator.HA_UNIQUE_ID]), json.dumps(message), False) def _publish_ha_device_rssi_config(self, device: Device, device_config: TranslatorDeviceConfig): # Generate message that can be used to automatically configure a sensor # for the device's RSSI in Home Assistant using MQTT Discovery message = { Translator.HA_NAME: device_config.ha_name_rssi, Translator.HA_UNIQUE_ID: '{}_{:06x}_RSSI'.format(PROJECT_NAME, device.device_id), Translator.HA_ICON: Translator.HA_ICON_RSSI, Translator.HA_STATE_TOPIC: '{}/{}'.format(device_config.topic, Device.PROP_RSSI_DB), Translator.HA_UNIT_OF_MEASUREMENT: Translator.HA_UOM_RSSI, Translator.HA_AVAILABILITY_TOPIC: '{}/{}'.format(self._config.translator.baseunit.topic, BaseUnit.PROP_IS_CONNECTED), Translator.HA_PAYLOAD_AVAILABLE: str(True), Translator.HA_PAYLOAD_NOT_AVAILABLE: str(False), } self._publish( '{}/{}/{}/config'.format( self._config.translator.ha_discovery_prefix, Translator.HA_PLATFORM_SENSOR, message[Translator.HA_UNIQUE_ID]), json.dumps(message), False) def _publish_ha_device_battery_config( self, device: Device, device_config: TranslatorDeviceConfig): # Generate message that can be used to automatically configure a binary # sensor for the device's battery state in Home Assistant using # MQTT Discovery message = { Translator.HA_NAME: device_config.ha_name_battery, Translator.HA_UNIQUE_ID: '{}_{:06x}_battery'.format(PROJECT_NAME, device.device_id), Translator.HA_DEVICE_CLASS: Translator.HA_DC_BATTERY, Translator.HA_PAYLOAD_ON: str(DeviceEventCode.BatteryLow), Translator.HA_PAYLOAD_OFF: str(DeviceEventCode.PowerOnReset), Translator.HA_STATE_TOPIC: '{}/event_code'.format(device_config.topic), Translator.HA_AVAILABILITY_TOPIC: '{}/{}'.format(self._config.translator.baseunit.topic, BaseUnit.PROP_IS_CONNECTED), Translator.HA_PAYLOAD_AVAILABLE: str(True), Translator.HA_PAYLOAD_NOT_AVAILABLE: str(False), } self._publish( '{}/{}/{}/config'.format( self._config.translator.ha_discovery_prefix, Translator.HA_PLATFORM_BINARY_SENSOR, message[Translator.HA_UNIQUE_ID]), json.dumps(message), False) def _publish_ha_switch_config(self, switch_number: SwitchNumber, switch_config: TranslatorSwitchConfig): # Generate message that can be used to automatically configure the # switch in Home Assistant using MQTT Discovery message = { Translator.HA_NAME: switch_config.ha_name, Translator.HA_UNIQUE_ID: '{}_{}'.format(PROJECT_NAME, str(switch_number).lower()), Translator.HA_STATE_TOPIC: switch_config.topic, Translator.HA_COMMAND_TOPIC: '{}/{}'.format(switch_config.topic, Translator.TOPIC_SET), Translator.HA_PAYLOAD_ON: str(OnOff.On), Translator.HA_PAYLOAD_OFF: str(OnOff.Off), Translator.HA_AVAILABILITY_TOPIC: '{}/{}'.format(self._config.translator.baseunit.topic, BaseUnit.PROP_IS_CONNECTED), Translator.HA_PAYLOAD_AVAILABLE: str(True), Translator.HA_PAYLOAD_NOT_AVAILABLE: str(False), } self._publish( '{}/{}/{}/config'.format( self._config.translator.ha_discovery_prefix, Translator.HA_PLATFORM_SWITCH, message[Translator.HA_UNIQUE_ID]), json.dumps(message), False) def _publish(self, topic: str, payload: Any, retain: bool) -> None: self._mqtt.publish(topic, payload, QOS_1, retain) def _on_message_baseunit(self, subscribetopic: SubscribeTopic, message: MQTTMessage) -> None: if subscribetopic.args == BaseUnit.PROP_OPERATION_MODE: # Set operation mode name = None if not message.payload else message.payload.decode() operation_mode = OperationMode.parse_name(name) if operation_mode is None: _LOGGER.warning("Cannot set operation_mode to '%s'", name) return if operation_mode == OperationMode.Disarm and \ self._state == BaseUnitState.Disarm and \ self._ha_state == Translator.HA_STATE_TRIGGERED: # Special case to ensure HA can return from triggered state # when triggered by an alarm in Disarm mode (eg. panic, # tamper)... the set disarm operation will not generate a # response from the base unit as there is no change, so we # need to reset 'ha_state' here. _LOGGER.debug("Resetting triggered ha_state in disarmed mode") self._ha_state = Translator.HA_STATE_DISARMED self._publish( '{}/{}'.format(self._config.translator.baseunit.topic, Translator.TOPIC_HASTATE), self._ha_state, True) self._loop.create_task( self._baseunit.async_set_operation_mode(operation_mode)) else: raise NotImplementedError def _on_message_clear_status(self, subscribetopic: SubscribeTopic, message: MQTTMessage) -> None: # Clear the alarm/warning LEDs on base unit and stop siren self._loop.create_task(self._baseunit.async_clear_status()) def _on_message_set_datetime(self, subscribetopic: SubscribeTopic, message: MQTTMessage) -> None: # Set remote date/time to specified date/time (or current if None) value = None if not message.payload else message.payload.decode() if value: value = dateutil.parser.parse(value) self._loop.create_task(self._baseunit.async_set_datetime(value)) def _on_message_switch(self, subscribetopic: SubscribeTopic, message: MQTTMessage) -> None: # Turn a switch on / off switch_number = subscribetopic.args name = None if not message.payload else message.payload.decode() state = OnOff.parse_name(name) if state is None: _LOGGER.warning("Cannot set switch %s to '%s'", switch_number, name) return self._loop.create_task( self._baseunit.async_set_switch_state(switch_number, bool(state.value))) def _on_ha_message(self, subscribetopic: SubscribeTopic, message: MQTTMessage) -> None: # When Home Assistant comes online, publish our configuration to it payload = None if not message.payload else message.payload.decode() if not payload: return if payload == self._config.translator.ha_birth_payload: self._publish_ha_config()
def subscribe_and_callback( callback: Callable[[MQTTMessage], None], topics: str | list[str], hostname: str = leader_hostname, last_will: dict = None, job_name: str = None, allow_retained: bool = True, **mqtt_kwargs, ) -> Client: """ Creates a new thread, wrapping around paho's subscribe.callback. Callbacks only accept a single parameter, message. Parameters ------------- last_will: dict a dictionary describing the last will details: topic, qos, retain, msg. job_name: Optional: provide the job name, and logging will include it. allow_retained: bool if True, all messages are allowed, including messages that the broker has retained. Note that client can fire a msg with retain=True, but because the broker is serving it to a subscriber "fresh", it will have retain=False on the client side. More here: https://github.com/eclipse/paho.mqtt.python/blob/master/src/paho/mqtt/client.py#L364 """ assert callable( callback ), "callback should be callable - do you need to change the order of arguments?" def on_connect(client: Client, userdata: dict, *args): client.subscribe(userdata["topics"]) def wrap_callback(actual_callback: Callable) -> Callable: def _callback(client: Client, userdata: dict, message): try: if not allow_retained and message.retain: return return actual_callback(message) except Exception as e: from pioreactor.logging import create_logger logger = create_logger(userdata.get("job_name", "pioreactor")) logger.error(e, exc_info=True) raise e return _callback topics = [topics] if isinstance(topics, str) else topics userdata = { "topics": [(topic, mqtt_kwargs.pop("qos", 0)) for topic in topics], "job_name": job_name, } client = Client(userdata=userdata) client.on_connect = on_connect client.on_message = wrap_callback(callback) if last_will is not None: client.will_set(**last_will) client.connect(leader_hostname, **mqtt_kwargs) client.loop_start() return client
class SphinxApp: _updated = False _hue = 0.0 _saturation = 0.0 _brightness = 0.0 lamp_is_on = 0 #def _get_hue(self): return self._hue #def _set_hue(self, value): self._hue = value #def _get_saturation(self): return self._saturation #def _set_saturation(self, value): self._saturation = value #def _get_brightness(self): return self._brightness #def _set_brightness(self, value): self._brightness = value hue = 0 # AliasProperty(_get_hue, _set_hue, bind=['_hue']) saturation = 0 # AliasProperty(_get_saturation, _set_saturation, bind=['_saturation']) brightness = 0 # AliasProperty(_get_brightness, _set_brightness, bind=['_brightness']) gpio17_pressed = 0 # BooleanProperty(False) device_associated = 0 # BooleanProperty(True) def on_start(self): self._publish_clock = None self.mqtt_broker_bridged = False self._associated = True self.association_code = None self.mqtt = Client(client_id=MQTT_CLIENT_ID) self.mqtt.enable_logger() self.mqtt.will_set(client_state_topic(MQTT_CLIENT_ID), "0", qos=2, retain=True) self.mqtt.on_connect = self.on_connect self.mqtt.connect(MQTT_BROKER_HOST, port=MQTT_BROKER_PORT, keepalive=MQTT_BROKER_KEEP_ALIVE_SECS) print("broker host", MQTT_BROKER_HOST, " mqtt broker port", MQTT_BROKER_PORT) #self.mqtt.loop_forever() # self.mqtt.loop_start() #self.set_up_GPIO_and_network_status_popup() #self.associated_status_popup = self._build_associated_status_popup() #self.associated_status_popup.bind(on_open=self.update_popup_associated) Clock.schedule_interval(self._poll_associated, 0.1) print("hue", self._hue, self.hue) print("saturation", self._saturation, self.saturation) print("brightness", self._brightness, self.brightness) print("onoff", self.lamp_is_on, Clock) def _build_associated_status_popup(self): return Popup(title='Associate your Lamp', content=Label(text='Msg here', font_size='30sp'), size_hint=(1, 1), auto_dismiss=False) def on_hue(self, instance, value): if self._updatingUI: return if self._publish_clock is None: self._publish_clock = Clock.schedule_once( lambda dt: self._update_leds(), 0.01) def on_saturation(self, instance, value): if self._updatingUI: return if self._publish_clock is None: self._publish_clock = Clock.schedule_once( lambda dt: self._update_leds(), 0.01) def on_brightness(self, instance, value): if self._updatingUI: return if self._publish_clock is None: self._publish_clock = Clock.schedule_once( lambda dt: self._update_leds(), 0.01) def on_lamp_is_on(self, instance, value): if self._updatingUI: return if self._publish_clock is None: self._publish_clock = Clock.schedule_once( lambda dt: self._update_leds(), 0.01) def on_connect(self, client, userdata, flags, rc): print("lampi_sphinx_app on_connect") self.mqtt.publish(client_state_topic(MQTT_CLIENT_ID), b"1", qos=2, retain=True) self.mqtt.message_callback_add(TOPIC_LAMP_CHANGE_NOTIFICATION, self.receive_new_lamp_state) self.mqtt.message_callback_add(broker_bridge_connection_topic(), self.receive_bridge_connection_status) self.mqtt.message_callback_add(TOPIC_LAMP_ASSOCIATED, self.receive_associated) self.mqtt.subscribe(broker_bridge_connection_topic(), qos=1) self.mqtt.subscribe(TOPIC_LAMP_CHANGE_NOTIFICATION, qos=1) self.mqtt.subscribe(TOPIC_LAMP_ASSOCIATED, qos=2) def _poll_associated(self, dt): # this polling loop allows us to synchronize changes from the # MQTT callbacks (which happen in a different thread) to the # Kivy UI print("lampi_sphinx_app _poll_associated") self.device_associated = self._associated def receive_associated(self, client, userdata, message): print("lampi_sphinx_app receive_assocaited") # this is called in MQTT event loop thread new_associated = json.loads(message.payload.decode('utf-8')) if self._associated != new_associated['associated']: if not new_associated['associated']: self.association_code = new_associated['code'] else: self.association_code = None self._associated = new_associated['associated'] def on_device_associated(self, instance, value): if value: self.associated_status_popup.dismiss() else: self.associated_status_popup.open() def update_popup_associated(self, instance): code = self.association_code[0:6] instance.content.text = ("Please use the\n" "following code\n" "to associate\n" "your device\n" "on the Web\n{}".format(code)) def receive_bridge_connection_status(self, client, userdata, message): print("lampi_sphinx_app receive_bridge_connection_status", userdata, message) # monitor if the MQTT bridge to our cloud broker is up if message.payload == b"1": self.mqtt_broker_bridged = True else: self.mqtt_broker_bridged = False def update_new_config(self, config): msg = { 'color': { 'h': self.hue, 's': self.saturation }, 'brightness': self.brightness, 'on': self.lamp_is_on, 'client': MQTT_CLIENT_ID } print("DEBUG update_new_config", config) print("current config:", msg) for piece in config: if type(piece) == type({}): print("piece", piece) for key in piece.keys(): if 'h' == key: print("Setting hue to:", piece[key]) self.hue = piece[key] if 's' == key: print("Setting sat to:", piece[key]) self.saturation = piece[key] if 'b' == key: print("Setting brightness to:", piece[key]) self.brightness = piece[key] elif type(piece) == type(""): if piece.find("LAMPI SET POWER") != -1: if piece.find("0") != -1: self.lamp_is_on = False elif piece.find("1") != -1: self.lamp_is_on = True else: print("Can't detect piece1!", piece) elif piece.find("TOGGLE") != -1: self.lamp_is_on = not self.lamp_is_on else: print("Can't detect piece2!", piece) msg = { 'color': { 'h': self.hue, 's': self.saturation }, 'brightness': self.brightness, 'on': self.lamp_is_on, 'client': MQTT_CLIENT_ID } self._update_leds() print("config now:", msg) # if 'color' in new_state: # self.hue = new_state['color']['h'] # self.saturation = new_state['color']['s'] # if 'brightness' in new_state: # self.brightness = new_state['brightness'] # if 'on' in new_state: # self.lamp_is_on = new_state['on'] def receive_new_lamp_state(self, client, userdata, message): print("lampi_sphinx_app receive_new_lamp_state", userdata, message) new_state = json.loads(message.payload.decode('utf-8')) self._update_ui(new_state) #Clock.schedule_once(lambda dt: self._update_ui(new_state), 0.01) print("updateUI Scheduled!") def _update_ui(self, new_state): print("lamp_sphinx_app _update_ui", new_state) if self._updated and new_state['client'] == MQTT_CLIENT_ID: # ignore updates generated by this client, except the first to # make sure the UI is syncrhonized with the lamp_service return try: if 'color' in new_state: self.hue = new_state['color']['h'] self.saturation = new_state['color']['s'] if 'brightness' in new_state: self.brightness = new_state['brightness'] if 'on' in new_state: self.lamp_is_on = new_state['on'] finally: pass self._updated = True def _update_leds(self): msg = { 'color': { 'h': self.hue, 's': self.saturation }, 'brightness': self.brightness, 'on': self.lamp_is_on, 'client': MQTT_CLIENT_ID } self.mqtt.publish(TOPIC_SET_LAMP_CONFIG, json.dumps(msg).encode('utf-8'), qos=1) self._publish_clock = None
class Mqtt(): """Main Mqtt class. :param app: flask application object :param connect_async: if True then connect_aync will be used to connect to MQTT broker :param mqtt_logging: if True then messages from MQTT client will be logged """ def __init__(self, app=None, connect_async=False, mqtt_logging=False): # type: (Flask, bool, bool) -> None self.app = app self._connect_async = connect_async # type: bool self._connect_handler = None # type: Optional[Callable] self._disconnect_handler = None # type: Optional[Callable] self.topics = {} # type: Dict[str, TopicQos] self.connected = False self.client = Client() if mqtt_logging: self.client.enable_logger(logger) if app is not None: self.init_app(app) def init_app(self, app): # type: (Flask) -> None """Init the Flask-MQTT addon.""" self.client_id = app.config.get("MQTT_CLIENT_ID", "") self.clean_session = app.config.get("MQTT_CLEAN_SESSION", True) if isinstance(self.client_id, unicode): self.client._client_id = self.client_id.encode('utf-8') else: self.client._client_id = self.client_id self.client._clean_session = self.clean_session self.client._transport = app.config.get("MQTT_TRANSPORT", "tcp").lower() self.client._protocol = app.config.get("MQTT_PROTOCOL_VERSION", MQTTv311) self.client.on_connect = self._handle_connect self.client.on_disconnect = self._handle_disconnect self.username = app.config.get("MQTT_USERNAME") self.password = app.config.get("MQTT_PASSWORD") self.broker_url = app.config.get("MQTT_BROKER_URL", "localhost") self.broker_port = app.config.get("MQTT_BROKER_PORT", 1883) self.tls_enabled = app.config.get("MQTT_TLS_ENABLED", False) self.keepalive = app.config.get("MQTT_KEEPALIVE", 60) self.last_will_topic = app.config.get("MQTT_LAST_WILL_TOPIC") self.last_will_message = app.config.get("MQTT_LAST_WILL_MESSAGE") self.last_will_qos = app.config.get("MQTT_LAST_WILL_QOS", 0) self.last_will_retain = app.config.get("MQTT_LAST_WILL_RETAIN", False) if self.tls_enabled: self.tls_ca_certs = app.config["MQTT_TLS_CA_CERTS"] self.tls_certfile = app.config.get("MQTT_TLS_CERTFILE") self.tls_keyfile = app.config.get("MQTT_TLS_KEYFILE") self.tls_cert_reqs = app.config.get("MQTT_TLS_CERT_REQS", ssl.CERT_REQUIRED) self.tls_version = app.config.get("MQTT_TLS_VERSION", ssl.PROTOCOL_TLSv1) self.tls_ciphers = app.config.get("MQTT_TLS_CIPHERS") self.tls_insecure = app.config.get("MQTT_TLS_INSECURE", False) # set last will message if self.last_will_topic is not None: self.client.will_set( self.last_will_topic, self.last_will_message, self.last_will_qos, self.last_will_retain, ) self._connect() def _connect(self): # type: () -> None if self.username is not None: self.client.username_pw_set(self.username, self.password) # security if self.tls_enabled: self.client.tls_set( ca_certs=self.tls_ca_certs, certfile=self.tls_certfile, keyfile=self.tls_keyfile, cert_reqs=self.tls_cert_reqs, tls_version=self.tls_version, ciphers=self.tls_ciphers, ) if self.tls_insecure: self.client.tls_insecure_set(self.tls_insecure) if self._connect_async: # if connect_async is used self.client.connect_async(self.broker_url, self.broker_port, keepalive=self.keepalive) else: res = self.client.connect(self.broker_url, self.broker_port, keepalive=self.keepalive) if res == 0: logger.debug("Connected client '{0}' to broker {1}:{2}".format( self.client_id, self.broker_url, self.broker_port)) else: logger.error( "Could not connect to MQTT Broker, Error Code: {0}".format( res)) self.client.loop_start() def _disconnect(self): # type: () -> None self.client.loop_stop() self.client.disconnect() logger.debug('Disconnected from Broker') def _handle_connect(self, client, userdata, flags, rc): # type: (Client, Any, Dict, int) -> None if rc == MQTT_ERR_SUCCESS: self.connected = True for key, item in self.topics.items(): self.client.subscribe(topic=item.topic, qos=item.qos) if self._connect_handler is not None: self._connect_handler(client, userdata, flags, rc) def _handle_disconnect(self, client, userdata, rc): # type: (str, Any, int) -> None self.connected = False if self._disconnect_handler is not None: self._disconnect_handler() def on_topic(self, topic): # type: (str) -> Callable """Decorator. Decorator to add a callback function that is called when a certain topic has been published. The callback function is expected to have the following form: `handle_topic(client, userdata, message)` :parameter topic: a string specifying the subscription topic to subscribe to The topic still needs to be subscribed via mqtt.subscribe() before the callback function can be used to handle a certain topic. This way it is possible to subscribe and unsubscribe during runtime. **Example usage:**:: app = Flask(__name__) mqtt = Mqtt(app) mqtt.subscribe('home/mytopic') @mqtt.on_topic('home/mytopic') def handle_mytopic(client, userdata, message): print('Received message on topic {}: {}' .format(message.topic, message.payload.decode())) """ def decorator(handler): # type: (Callable[[str], None]) -> Callable[[str], None] self.client.message_callback_add(topic, handler) return handler return decorator def subscribe(self, topic, qos=0): # type: (str, int) -> Tuple[int, int] """ Subscribe to a certain topic. :param topic: a string specifying the subscription topic to subscribe to. :param qos: the desired quality of service level for the subscription. Defaults to 0. :rtype: (int, int) :result: (result, mid) A topic is a UTF-8 string, which is used by the broker to filter messages for each connected client. A topic consists of one or more topic levels. Each topic level is separated by a forward slash (topic level separator). The function returns a tuple (result, mid), where result is MQTT_ERR_SUCCESS to indicate success or (MQTT_ERR_NO_CONN, None) if the client is not currently connected. mid is the message ID for the subscribe request. The mid value can be used to track the subscribe request by checking against the mid argument in the on_subscribe() callback if it is defined. **Topic example:** `myhome/groundfloor/livingroom/temperature` """ # TODO: add support for list of topics # don't subscribe if already subscribed # try to subscribe result, mid = self.client.subscribe(topic=topic, qos=qos) # if successful add to topics if result == MQTT_ERR_SUCCESS: self.topics[topic] = TopicQos(topic=topic, qos=qos) logger.debug('Subscribed to topic: {0}, qos: {1}'.format( topic, qos)) else: logger.error('Error {0} subscribing to topic: {1}'.format( result, topic)) return (result, mid) def unsubscribe(self, topic): # type: (str) -> Optional[Tuple[int, int]] """ Unsubscribe from a single topic. :param topic: a single string that is the subscription topic to unsubscribe from :rtype: (int, int) :result: (result, mid) Returns a tuple (result, mid), where result is MQTT_ERR_SUCCESS to indicate success or (MQTT_ERR_NO_CONN, None) if the client is not currently connected. mid is the message ID for the unsubscribe request. The mid value can be used to track the unsubscribe request by checking against the mid argument in the on_unsubscribe() callback if it is defined. """ # don't unsubscribe if not in topics if topic in self.topics: result, mid = self.client.unsubscribe(topic) if result == MQTT_ERR_SUCCESS: self.topics.pop(topic) logger.debug('Unsubscribed from topic: {0}'.format(topic)) else: logger.debug('Error {0} unsubscribing from topic: {1}'.format( result, topic)) # if successful remove from topics return result, mid return None def unsubscribe_all(self): # type: () -> None """Unsubscribe from all topics.""" topics = list(self.topics.keys()) for topic in topics: self.unsubscribe(topic) def publish(self, topic, payload=None, qos=0, retain=False): # type: (str, bytes, int, bool) -> Tuple[int, int] """ Send a message to the broker. :param topic: the topic that the message should be published on :param payload: the actual message to send. If not given, or set to None a zero length message will be used. Passing an int or float will result in the payload being converted to a string representing that number. If you wish to send a true int/float, use struct.pack() to create the payload you require. :param qos: the quality of service level to use :param retain: if set to True, the message will be set as the "last known good"/retained message for the topic :returns: Returns a tuple (result, mid), where result is MQTT_ERR_SUCCESS to indicate success or MQTT_ERR_NO_CONN if the client is not currently connected. mid is the message ID for the publish request. """ if not self.connected: self.client.reconnect() result, mid = self.client.publish(topic, payload, qos, retain) if result == MQTT_ERR_SUCCESS: logger.debug('Published topic {0}: {1}'.format(topic, payload)) else: logger.error('Error {0} publishing topic {1}'.format( result, topic)) return (result, mid) def on_connect(self): # type: () -> Callable """Decorator. Decorator to handle the event when the broker responds to a connection request. Only the last decorated function will be called. """ def decorator(handler): # type: (Callable) -> Callable self._connect_handler = handler return handler return decorator def on_disconnect(self): # type: () -> Callable """Decorator. Decorator to handle the event when client disconnects from broker. Only the last decorated function will be called. """ def decorator(handler): # type: (Callable) -> Callable self._disconnect_handler = handler return handler return decorator def on_message(self): # type: () -> Callable """Decorator. Decorator to handle all messages that have been subscribed and that are not handled via the `on_message` decorator. **Note:** Unlike as written in the paho mqtt documentation this callback will not be called if there exists an topic-specific callback added by the `on_topic` decorator. **Example Usage:**:: @mqtt.on_message() def handle_messages(client, userdata, message): print('Received message on topic {}: {}' .format(message.topic, message.payload.decode())) """ def decorator(handler): # type: (Callable) -> Callable self.client.on_message = handler return handler return decorator def on_publish(self): # type: () -> Callable """Decorator. Decorator to handle all messages that have been published by the client. **Example Usage:**:: @mqtt.on_publish() def handle_publish(client, userdata, mid): print('Published message with mid {}.' .format(mid)) """ def decorator(handler): # type: (Callable) -> Callable self.client.on_publish = handler return handler return decorator def on_subscribe(self): # type: () -> Callable """Decorate a callback function to handle subscritions. **Usage:**:: @mqtt.on_subscribe() def handle_subscribe(client, userdata, mid, granted_qos): print('Subscription id {} granted with qos {}.' .format(mid, granted_qos)) """ def decorator(handler): # type: (Callable) -> Callable self.client.on_subscribe = handler return handler return decorator def on_unsubscribe(self): # type: () -> Callable """Decorate a callback funtion to handle unsubscribtions. **Usage:**:: @mqtt.unsubscribe() def handle_unsubscribe(client, userdata, mid) print('Unsubscribed from topic (id: {})' .format(mid)') """ def decorator(handler): # type: (Callable) -> Callable self.client.on_unsubscribe = handler return handler return decorator def on_log(self): # type: () -> Callable """Decorate a callback function to handle MQTT logging. **Example Usage:** :: @mqtt.on_log() def handle_logging(client, userdata, level, buf): print(client, userdata, level, buf) """ def decorator(handler): # type: (Callable) -> Callable self.client.on_log = handler return handler return decorator
def on_connect(client, userdata, flags, rc): print('MQTT connect: {}'.format(connack_string(rc))) client.publish(status_topic, status_connected, retain=True) def on_disconnect(client, userdata, rc): print('MQTT disconnect: {}'.format(error_string(rc))) def on_message(client, userdata, message): pass # not used client = Client() client.will_set(status_topic, status_error, retain=True) client.connect_async(mqtt_server) # client.connect(mqtt_server) client.on_connect = on_connect client.on_disconnect = on_disconnect client.on_message = on_message client.loop_start() @atexit def run_at_exit(): client.publish(status_topic, status_disconnected, retain=True) client.loop_stop() client.disconnect()
class MQTTManager: def __init__(self, configuration_manager: ConfigurationManager, callback): self._mqtt_client = None # type: Optional[Client] self._configuration_manager = configuration_manager self._topic_subscribe = None self._topic_publish = None self._topic_lwt = None self._callback = callback self._is_ready = False this = self def mqtt_on_connect(client: Client, userdata, flags, rc): _LOGGER.error("MQTT Client connected") client.subscribe(this._topic_subscribe) def mqtt_on_disconnect(client: Client, userdata, flags, rc): _LOGGER.error("MQTT Client disconnected") this._is_ready = False this.connect() def mqtt_on_message(client: Client, userdata, message: MQTTMessage): result = self._callback(message.payload) this.publish_status(result) self._mqtt_on_connect = mqtt_on_connect self._mqtt_on_disconnect = mqtt_on_disconnect self._mqtt_on_message = mqtt_on_message def _get_topic(self, key): topic = path.join(self._configuration_manager.mqtt_topic, key) return topic def connect(self): if self._configuration_manager.mqtt_host: self._topic_publish = self._get_topic(CMD_STATUS) self._topic_subscribe = self._get_topic(CMD_ARM) self._topic_lwt = self._get_topic("LWT") self._mqtt_client = Client( client_id=self._configuration_manager.mqtt_client_id, clean_session=True) self._mqtt_client.on_connect = self._mqtt_on_connect self._mqtt_client.on_disconnect = self._mqtt_on_disconnect self._mqtt_client.on_message = self._mqtt_on_message if (self._configuration_manager.mqtt_username and self._configuration_manager.mqtt_password): self._mqtt_client.username_pw_set( self._configuration_manager.mqtt_username, self._configuration_manager.mqtt_password, ) self._mqtt_client.will_set(self._topic_lwt, payload="offline", retain=True) while True: try: self._mqtt_client.connect( self._configuration_manager.mqtt_host, self._configuration_manager.mqtt_port, ) self._is_ready = True except (socket.timeout, OSError): _LOGGER.exception( "Failed to connect to MQTT broker. Retrying in 5 seconds..." ) sleep(5) else: break self._mqtt_publish_lwt_online() self._mqtt_client.loop_start() def publish_status(self, status: dict) -> None: if self._is_ready: self._mqtt_client.publish(self._topic_publish, payload=to_json(status)) def _mqtt_publish_lwt_online(self) -> None: if self._is_ready: self._mqtt_client.publish(self._topic_lwt, payload="online", retain=True)
class LampiApp(App): _updated = False _updatingUI = False _hue = NumericProperty() _saturation = NumericProperty() _brightness = NumericProperty() lamp_is_on = BooleanProperty() temperature = NumericProperty(0) rain = BooleanProperty(False) snow = BooleanProperty(False) def _get_hue(self): return self._hue def _set_hue(self, value): self._hue = value def _get_saturation(self): return self._saturation def _set_saturation(self, value): self._saturation = value def _get_brightness(self): return self._brightness def _set_brightness(self, value): self._brightness = value hue = AliasProperty(_get_hue, _set_hue, bind=['_hue']) saturation = AliasProperty(_get_saturation, _set_saturation, bind=['_saturation']) brightness = AliasProperty(_get_brightness, _set_brightness, bind=['_brightness']) gpio17_pressed = BooleanProperty(False) device_associated = BooleanProperty(True) def _update_temperature(self): pass def on_start(self): self._publish_clock = None self.mqtt_broker_bridged = False self._associated = True self.association_code = None self.mqtt = Client(client_id=MQTT_CLIENT_ID) self.mqtt.will_set(client_state_topic(MQTT_CLIENT_ID), "0", qos=2, retain=True) self.mqtt.on_connect = self.on_connect self.mqtt.connect(MQTT_BROKER_HOST, port=MQTT_BROKER_PORT, keepalive=MQTT_BROKER_KEEP_ALIVE_SECS) self.mqtt.loop_start() self.set_up_GPIO_and_device_status_popup() self.associated_status_popup = self._build_associated_status_popup() self.associated_status_popup.bind(on_open=self.update_popup_associated) k = "2d19784349d26a0853b4c54397d7ff07" owm = pyowm.OWM(k) observation = owm.weather_at_place('Cleveland, US') w = observation.get_weather() self.temperature = w.get_temperature('fahrenheit')['temp_min'] if w.get_rain() > 50: self.rain = True else: self.rain = False if w.get_snow(): self.snow = True else: self.snow = False Clock.schedule_interval(self._poll_associated, 0.1) #lbl = Label(text="test") #Clock.schedule_once(lambda dt: self.show_marks(lbl), 1) #self.add_widget(lbl) #self.ids['temperature'].text='test1' #self.temperature = ObjectProperty(None) #self.temperature.text = "test" def _build_associated_status_popup(self): return Popup(title='Associate your Lamp', content=Label(text='Msg here', font_size='30sp'), size_hint=(1, 1), auto_dismiss=False) def on_hue(self, instance, value): if self._updatingUI: return if self._publish_clock is None: self._publish_clock = Clock.schedule_once( lambda dt: self._update_leds(), 0.01) def on_saturation(self, instance, value): if self._updatingUI: return if self._publish_clock is None: self._publish_clock = Clock.schedule_once( lambda dt: self._update_leds(), 0.01) def on_brightness(self, instance, value): if self._updatingUI: return if self._publish_clock is None: self._publish_clock = Clock.schedule_once( lambda dt: self._update_leds(), 0.01) def on_lamp_is_on(self, instance, value): if self._updatingUI: return if self._publish_clock is None: self._publish_clock = Clock.schedule_once( lambda dt: self._update_leds(), 0.01) def _create_event_record(self, element, value): return {'element': {'id': element, 'value': value}} def on_connect(self, client, userdata, flags, rc): self.mqtt.publish(client_state_topic(MQTT_CLIENT_ID), "1", qos=2, retain=True) self.mqtt.message_callback_add(TOPIC_LAMP_CHANGE_NOTIFICATION, self.receive_new_lamp_state) self.mqtt.message_callback_add(broker_bridge_connection_topic(), self.receive_bridge_connection_status) self.mqtt.message_callback_add(TOPIC_LAMP_ASSOCIATED, self.receive_associated) self.mqtt.subscribe(broker_bridge_connection_topic(), qos=1) self.mqtt.subscribe(TOPIC_LAMP_CHANGE_NOTIFICATION, qos=1) self.mqtt.subscribe(TOPIC_LAMP_ASSOCIATED, qos=2) #self.RelativeLayout.ids.temperature.text = "test" print("test") #print(self.__dict__.keys()) #print(self.root.ids.temperature.text) def on_message(self, client, userdata, msg): #temperature reading if "temperature" in msg.topic: temperature = msg.payload #is raining elif "rain" in msg.topic: if msg.payload == "1": pass else: pass #is cold elif "cold" in msg.topic: if msg.payload == "1": pass else: pass #is snowing elif "snow" in msg.topic: if msg.payload == "1": pass else: pass def _poll_associated(self, dt): # this polling loop allows us to synchronize changes from the # MQTT callbacks (which happen in a different thread) to the # Kivy UI self.device_associated = self._associated def receive_associated(self, client, userdata, message): # this is called in MQTT event loop thread new_associated = json.loads(message.payload) if self._associated != new_associated['associated']: if not new_associated['associated']: self.association_code = new_associated['code'] else: self.association_code = None self._associated = new_associated['associated'] def on_device_associated(self, instance, value): if value: self.associated_status_popup.dismiss() else: self.associated_status_popup.open() def update_popup_associated(self, instance): code = self.association_code[0:6] instance.content.text = ("Please use the\n" "following code\n" "to associate\n" "your device\n" "on the Web\n{}".format(code) ) def receive_bridge_connection_status(self, client, userdata, message): # monitor if the MQTT bridge to our cloud broker is up if message.payload == "1": self.mqtt_broker_bridged = True else: self.mqtt_broker_bridged = False def receive_new_lamp_state(self, client, userdata, message): new_state = json.loads(message.payload) Clock.schedule_once(lambda dt: self._update_ui(new_state), 0.01) def _update_ui(self, new_state): if self._updated and new_state['client'] == MQTT_CLIENT_ID: # ignore updates generated by this client, except the first to # make sure the UI is syncrhonized with the lamp_service return self._updatingUI = True try: if 'color' in new_state: self.hue = new_state['color']['h'] self.saturation = new_state['color']['s'] if 'brightness' in new_state: self.brightness = new_state['brightness'] if 'on' in new_state: self.lamp_is_on = new_state['on'] finally: self._updatingUI = False self._updated = True def _update_leds(self): msg = {'color': {'h': self._hue, 's': self._saturation}, 'brightness': self._brightness, 'on': self.lamp_is_on, 'client': MQTT_CLIENT_ID} self.mqtt.publish(TOPIC_SET_LAMP_CONFIG, json.dumps(msg), qos=1) self._publish_clock = None def set_up_GPIO_and_device_status_popup(self): self.pi = pigpio.pi() self.pi.set_mode(17, pigpio.INPUT) self.pi.set_pull_up_down(17, pigpio.PUD_UP) Clock.schedule_interval(self._poll_GPIO, 0.05) self.network_status_popup = self._build_network_status_popup() self.network_status_popup.bind(on_open=self.update_device_status_popup) def _build_network_status_popup(self): return Popup(title='Device Status', content=Label(text='IP ADDRESS WILL GO HERE'), size_hint=(1, 1), auto_dismiss=False) def update_device_status_popup(self, instance): interface = "wlan0" ipaddr = lampi_util.get_ip_address(interface) deviceid = lampi_util.get_device_id() msg = ("Version: {}\n" "{}: {}\n" "DeviceID: {}\n" "Broker Bridged: {}\n" "threaded" ).format( LAMPI_APP_VERSION, interface, ipaddr, deviceid, self.mqtt_broker_bridged) instance.content.text = msg def on_gpio17_pressed(self, instance, value): if value: self.network_status_popup.open() else: self.network_status_popup.dismiss() def _poll_GPIO(self, dt): # GPIO17 is the rightmost button when looking front of LAMPI self.gpio17_pressed = not self.pi.read(17)