Example #1
0
    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}))
Example #2
0
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
Example #3
0
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)
Example #4
0
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()
Example #5
0
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
Example #6
0
        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/#")
Example #7
0
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)
Example #8
0
    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
Example #10
0
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()
Example #11
0
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)
Example #12
0
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
Example #13
0
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()
Example #14
0
 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.")
Example #15
0
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)
Example #16
0
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)
Example #17
0
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()
Example #18
0
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
Example #20
0
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
Example #21
0
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()

Example #22
0
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)