Example #1
0
class BugfixClient(PAHO_MQTT_Client):
    def connect(self):
        homie.mqtt.mqtt_base.MQTT_Base.connect(self)
        #self.mqtt_client = MQTTClientWrapper(client_id=self.mqtt_settings['MQTT_CLIENT_ID'])
        self.mqtt_client = Client(client_id=self.mqtt_settings['MQTT_CLIENT_ID'])
        self.mqtt_client.on_connect = self._on_connect
        self.mqtt_client.on_message = self._on_message
        #self.mqtt_client.on_publish = self._on_publish
        self.mqtt_client.on_disconnect = self._on_disconnect
        self.mqtt_client.enable_logger(homie.mqtt.paho_mqtt_client.mqtt_logger)
        self.mqtt_client.enable_logger()
        if self.mqtt_settings ['MQTT_USERNAME']:
            self.mqtt_client.username_pw_set(
                    self.mqtt_settings ['MQTT_USERNAME'],
                    password=self.mqtt_settings ['MQTT_PASSWORD']
            )
        try:
            self.mqtt_client.connect(
                self.mqtt_settings ['MQTT_BROKER'],
                port=self.mqtt_settings ['MQTT_PORT'],
                keepalive=self.mqtt_settings ['MQTT_KEEPALIVE'],
            )
            self.mqtt_client.loop_start()
        except Exception as e:
            homie.mqtt.paho_mqtt_client.logger.warning ('MQTT Unable to connect to Broker {}'.format(e))
Example #2
0
def _create_mqtt_client(params):
    mqttc = Client(
        client_id=params.get("client_id", ''),
        clean_session=params.get('clean_session'),
        protocol=_to_protocol(params.get('protocol')),
        transport=_get_transport(params),
    )

    for name in _MQTT_NESTED_PARAMETER:
        if name not in params:
            continue
        logger.debug(f'invoke: {name}({params[name]})')
        getattr(mqttc, name)(**params[name])

    mqttc.enable_logger(logger)
    return mqttc
Example #3
0
class MQTTPublisher():
    def __init__(self):
        client_id = os.environ.get('MQTT_CLIENT_ID', socket.gethostname())
        self._client = Client(client_id)
        self._client.enable_logger(logger=logging)

        user = os.environ.get('MQTT_USER')
        password = os.environ.get('MQTT_PASSWORD')
        if user is not None and password is not None:
            self._client.username_pw_set(username=user, password=password)

        self._host = os.environ.get('MQTT_HOST', 'localhost')
        self._port = int(os.environ.get('MQTT_PORT', '1883'))
        self.connect()

    def _log_success_sent(self, topic, payload):
        logging.info(f'sent {payload} to topic {topic}')

    def _log_failed_sent(self, topic, payload):
        logging.error(f'failed to send {payload} to topic {topic}')

    def publish(self, topic, payload, retain):
        try:
            result = self._client.publish(topic=topic,
                                          payload=payload,
                                          retain=retain)

            if result[0] == 0:
                self._log_success_sent(topic, payload)
            else:
                self._log_failed_sent(topic, payload)
                time.sleep(5)
                self.connect()
                self.publish(topic, payload, retain)

        except Exception as e:
            logging.exception(e)
            time.sleep(5)
            self.connect()
            self.publish(topic, payload, retain)

    def connect(self):
        try:
            self._client.connect(self._host, self._port)
        except Exception as e:
            logging.exception(
                [f'failed to connect to MQTT, will retry later', e])
Example #4
0
    def __init__(self, client_id, config, wait=True):
        """
        initialize mqtt client
        :param client_id: client id
        :param config: keeper configuration
        :param wait: whether to wait for connection
        """

        self.logger = Logger()
        user = config.get("mqtt.user")
        pwd = config.get("mqtt.pass")
        client = Client(client_id=client_id)
        client.on_connect = self._on_connect
        client.on_disconnect = self._on_disconnect
        client.on_message = self._on_message
        client.enable_logger(self.logger)
        if user and pwd:
            client.username_pw_set(user, pwd)

        client.connect_async(config["mqtt.broker"], config["mqtt.port"], 30)
        self.client = client
        self.connected = False
        self.manager = None
        self.wait = wait
Example #5
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
class MqttClient:
    """A wrapper around the paho mqtt client specifically for device automation bus

    This wrapper is mean to handle connect/disconnect as well as sending and
    receiving messages.
    Clients can register handlers to be called when certain messages are received.
    Clients also use this class to publish messages onto the bus.
    """
    class MqttClientEvents:
        """Container for MqttClient thread events"""
        def __init__(self):
            self.connected_event = Event()
            self.disconnected_event = Event()
            self.apps_discovered_event = Event()
            self.capabilities_discovered_event = Event()

    def __init__(self, host, port):
        self.host = host
        self.port = port
        self.mqtt_client_events = MqttClient.MqttClientEvents()
        self.thread = None
        self.client = Client(userdata=self.mqtt_client_events)

        self.app_registry = {}
        self.app_registry_timer = None
        self.app_registry_timer_lock = Lock()

        self.capabilities_registry = {}
        self.capabilities_registry_timer = None
        self.capabilities_registry_timer_lock = Lock()

        self.topic_handlers = [{
            'topic': 'apps/+',
            'regex': 'apps/[^/]*',
            'handler': self.on_app_message
        }, {
            'topic': 'platform/+',
            'regex': 'platform/[^/]*',
            'handler': self.on_app_message
        }, {
            'topic': 'platform/telemetry/monitor/#',
            'regex': 'platform/telemetry/monitor/.*',
            'handler': self.on_monitor_message
        }]
        self.logger = logging.getLogger(__name__)

    def on_connect(self, client, mqtt_client_events, flags, rc):
        """Set the connected state and subscribe to existing known topics."""
        del mqtt_client_events, flags, rc  # Delete unused parameters to prevent warnings.
        for topic_handler in self.topic_handlers:
            client.subscribe(topic_handler["topic"])
        self.mqtt_client_events.connected_event.set()
        self.mqtt_client_events.disconnected_event.clear()

    def on_disconnect(self, client, user_data, rc):
        """Set the disconnected state."""
        del client, user_data, rc  # Delete unused parameters to prevent warnings.
        self.mqtt_client_events.connected_event.clear()
        self.mqtt_client_events.disconnected_event.set()

    def on_message(self, client, user_data, packet):
        """The main message dispatcher

        This function is called for all messages on all registered topics.
        It handles dispatching messages to the registered handlers.
        """
        del client, user_data  # Delete unused parameters to prevent warnings.
        try:
            if packet:
                self.logger.info("topic: " + packet.topic)
                # Message payload can come in as a char array instead of string.
                # Normalize it to a string for easier access by handlers.
                if type(packet.payload) is str:
                    message = packet.payload
                else:
                    message = packet.payload.decode("utf-8")
                self.logger.info("message: " + message)

                # Since this function receives messages for all topics, use the specified regex
                # to call the correct handler(s).
                for topic_handler in self.topic_handlers:
                    if re.fullmatch(topic_handler["regex"], packet.topic):
                        topic_handler["handler"](packet.topic, message)
        # Since paho eats all the exceptions, catch them here beforehand
        # and make sure a stack trace is printed.
        except Exception:
            traceback.print_exc()
            raise

    def on_app_message(self, topic, message):
        """Local handler for handling registration of media apps.

        This function receives a message for each registered media application.
        Since these messages are published as retained, normally this will be
        called with all the messages in on batch.
        To know that we have received all of the initial batch, use a simple timeout
        to measure how long it has been since the last message. When enough time goes
        by, allow callers to get the list of apps from the registry.
        """
        app = json.loads(message)
        app["app_id"] = os.path.basename(topic)
        self.app_registry.update({app["name"]: app})
        self.app_registry_timer_lock.acquire(True)
        if self.app_registry_timer:
            self.app_registry_timer.cancel()
            while self.app_registry_timer.is_alive():
                pass
        self.app_registry_timer = Timer(
            1.0, self.mqtt_client_events.apps_discovered_event.set).start()
        self.app_registry_timer_lock.release()

    def on_platform_message(self, topic, message):
        """Local handler for handling platform messages

        This function receives a message for each registered platform capability.
        """
        capability = json.loads(message)
        capability_id = os.path.basename(topic)
        self.capabilities_registry.update({capability_id: capability})

        self.capabilities_registry_timer_lock.acquire(True)
        if self.capabilities_registry_timer:
            self.capabilities_registry_timer.cancel()
            while self.capabilities_registry_timer.is_alive():
                pass
        self.capabilities_registry_timer = Timer(
            1.0,
            self.mqtt_client_events.capabilities_discovered_event.set).start()
        self.capabilities_registry_timer_lock.release()

    def on_monitor_message(self, topic, message):
        """Local handler for handling process monitor messages

        """
        message = json.loads(message)

    def get_discovered_apps(self):
        """Method to get the discovered apps.

        TODO: Get the app registry out of here into it's own class.
        """
        self.mqtt_client_events.apps_discovered_event.wait()
        self.app_registry_timer_lock.acquire(True)
        discovered_apps = self.app_registry.copy().values()
        self.app_registry_timer_lock.release()
        return discovered_apps

    def get_discovered_capabilities(self):
        """Method to get the discovered platform capabilities.

        TODO: Get the registry out of here into it's own class.
        """
        self.mqtt_client_events.capabilities_discovered_event.wait()
        self.capabilities_registry_timer_lock.acquire(True)
        discovered_capabilities = self.capabilities_registry.copy().values()
        self.capabilities_registry_timer_lock.release()
        return discovered_capabilities

    def _start(self):
        """Private start method that does th actual work of starting the client in a new thread."""
        self.client.enable_logger(self.logger)
        self.client.on_connect = self.on_connect
        self.client.on_message = self.on_message
        self.client.on_disconnect = self.on_disconnect
        self.client.connect(self.host, self.port)
        self.client.loop_start()
        self.mqtt_client_events.connected_event.wait(15.0)
        if not self.mqtt_client_events.connected_event.is_set():
            raise Exception("Connect timed out after 15 seconds.")

        self.mqtt_client_events.disconnected_event.wait()  # yes, forever

    def start(self):
        """Public start method used by callers to start the client."""
        self.thread = Thread(target=self._start)
        self.thread.start()

    def stop(self):
        """Stop method used to shutdown the client."""
        for topic_handler in self.topic_handlers:
            self.client.unsubscribe(topic_handler["topic"])
        self.client.disconnect()
        self.mqtt_client_events.apps_discovered_event.set()

    def publish(self, topic, message):
        """Publish a message to the specified topic."""
        self.logger.info("publish: Sending '{}' to topic '{}'".format(
            message, topic))
        self.client.publish(topic, message)

    def subscribe(self, topic, handler, regex=None):
        """Add a handler for a particular topic

        The handler is a function that will be called when a message is received on the specified topic.
        An optional regex can be provided to filter the topic even more.
        """
        if not regex:
            regex = topic
        self.topic_handlers.append({
            'topic': topic,
            'handler': handler,
            'regex': regex
        })
        self.client.subscribe(topic)
class HubCommunicationService:
    def __init__(self):
        self.client = None
        self.client_name = None
        self.rooms_service = None

    def initialize(self, configuration, rooms_service):
        self.rooms_service = rooms_service
        comm_config = configuration.get(CONFIG_CONNECTIVITY)
        self.client_name = comm_config.get(CONFIG_CONNECTIVITY_CLIENTNAME)
        self.client = Client(self.client_name)
        host = comm_config.get(CONFIG_CONNECTIVITY_MQTTIP)
        port = comm_config.get(CONFIG_CONNECTIVITY_MQTTPORT)
        self.client.enable_logger()
        self.client.on_message = self.on_message
        self.client.on_connect = self.on_connect
        self.client.connect(host=host, port=port)
        self.client.loop_start()

    def destroy(self):
        self.client.loop_stop()
        self.client.disconnect()

    def create_topic_name(self):
        # topic format /Actor/In/#
        return "{sep}{name}{sep}{tin}{sep}#".format(sep=NAME_SEPARATOR,
                                                    tin=TOPIC_IN,
                                                    name=self.client_name)

    def get_point_id(self, channel):
        if channel is None:
            return None
        topic_indicator = NAME_SEPARATOR + TOPIC_IN + NAME_SEPARATOR
        idx = channel.find(topic_indicator)
        if idx < 0:
            return None
        return channel[idx + len(topic_indicator) - 1:len(channel)]

    def is_admin_channel(self, channel):
        if channel is None:
            return False
        admin_indicator = NAME_SEPARATOR + TOPIC_ADMIN
        idx = channel.find(admin_indicator)
        if idx < 0:
            return False
        return True

    def get_channel(self, point_id):
        # topic format /Actor/Out/PointId
        return "{sep}{act}{sep}{tout}{id}".format(sep=NAME_SEPARATOR,
                                                  tout=TOPIC_OUT,
                                                  act=self.client_name,
                                                  id=point_id)

    def on_message(self, client, userdata, message):
        topic = message.topic
        payload = str(message.payload.decode("utf-8"))
        logging.debug("Msg rcv: {0} msg: {1}".format(topic, payload))
        self.process_message(channel=topic, message=payload)

    def on_connect(self, client, userdata, flags, rc):
        logging.info("Connected to mqtt service on {0}:{1} ".format(
            client._host, client._port))
        topic = self.create_topic_name()
        logging.info("Subscribing to topic {}".format(topic))
        self.client.subscribe(topic)

    def sendStatusUpdate(self, point_id, message):
        topic = self.get_channel(point_id)
        self.client.publish(topic=topic, payload=message)
        logging.debug("Msg snd: {0} msg: {1}".format(topic, message))

    def process_message(self, channel, message):
        if self.is_admin_channel(channel):
            self.process_admin_message(channel, message)
            return

        point_id = self.get_point_id(channel)
        if point_id is None:
            logging.info(
                "Point_id not determined for channel {0} and message {1}, skipped"
                .format(channel, message))
        else:
            self.rooms_service.updateStatus(point_id=point_id, message=message)

    def process_admin_message(self, channel, message):
        logging.info("Admin message received: channel {0} message {1}".format(
            channel, message))
        if message == COMMAND_RESET:
            self.rooms_service.reset()
        if message == COMMAND_NOTIFY_CURRENT_STATE:
            self.rooms_service.notify_current_state()
Example #8
0
            continue
        [[line, v]] = samples[k].items()
        topic = 'clients/' + USER + '/' + str(k)
        print('Sending mqtt with topic {}'.format(topic))
        msg_info = cb.publish(topic=topic, payload=str(v))
    cb.loop_stop()

# prepare
message_type = os.getenv('MESSAGE_TYPE', 'reddit')
DB_PATH = os.getenv('DB_PATH', '/reddit-comments/database.csv')
DATA = '/data'
USER = os.getenv('HOSTNAME', random_string())
PATH = ""

cb = Client('messenger' + USER)
cb.enable_logger()
gen_type = ''


if __name__ == "__main__":
    PATH = os.path.join(DATA, USER)
    try:
        os.mkdir(PATH)
    except FileExistsError:
        print('User already exists')
    cb.connect(host='mqtt', port=1883)
    scheduler = BlockingScheduler()
    job_mqtt = scheduler.add_job(publish_message_mqtt, 'interval', seconds=10)
    job = scheduler.add_job(generate_files, 'interval', seconds=15)
    scheduler.start()
Example #9
0
class HUB:
    def __init__(self, config_file='./hub_config.ini'):
        log_format = '<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> <level>{level}: {message}</level>'
        logger.remove()
        logger.add(sys.stdout, format=log_format, colorize=True)
        config = configparser.ConfigParser()
        if not os.path.isfile(config_file):
            logger.critical(f"Cannot find '{config_file}', aborting...")
            exit(1)
        config.read(config_file)
        logger.add(os.path.join(config['logging']['logs_path'],
                                'log_{time:YYYY-MM-DD}.log'),
                   format=log_format,
                   colorize=True,
                   compression='zip',
                   rotation='00:00')
        logger.info('Booting up...')
        self.mqtt_broker = config['mqtt']['mqtt_broker']
        self.mqtt_topic = config['mqtt']['mqtt_topic']
        self.scan_rate = int(config['scanning']['rate'])
        self.max_threads = int(config['scanning']['threads'])
        logger.info(
            f"[Scan configuration]:\nScan rate: {self.scan_rate}s\nScan threads: {self.max_threads}"
        )
        mqtt_id = config['mqtt']['mqtt_id']
        logger.info(
            f"[MQTT configuration]\nBroker: {self.mqtt_broker}\nTopic: {self.mqtt_topic}\nID: {mqtt_id}"
        )
        self.mqtt_client = Client(client_id=mqtt_id)
        self.mqtt_client.enable_logger(logger)
        self.mqtt_client.on_message = self.toggle_bulbs
        self.mqtt_client.on_connect = self.mqtt_subscribe
        self.mqtt_client.on_disconnect = self.mqtt_connect
        self.mqtt_client.loop_start()
        self.mqtt_connect()
        self.bulbs = []  # [{'hostname': '<>', 'ip': '<>'}]
        self.threads_buffer = []
        self.startup()
        self.loop()

    def mqtt_connect(self):
        logger.debug(f'Connecting to MQTT broker {self.mqtt_broker}...')
        try:
            response = self.mqtt_client.connect(host=self.mqtt_broker,
                                                port=1883)
        except Exception as e:
            logger.error(f"Can't connect to MQTT broker; {e}")
            response = 1
        if response != 0:
            logger.error(f'Not connected to MQTT broker {self.mqtt_broker}')

    def mqtt_subscribe(self, *args):
        self.mqtt_client.subscribe(topic=self.mqtt_topic, qos=1)
        logger.info(f'Subscribed to {self.mqtt_topic}')

    def get_subnet(self):
        ip_scanner = IPRoute()
        info = [{
            'iface': x['index'],
            'addr': x.get_attr('IFA_ADDRESS'),
            'mask': x['prefixlen']
        } for x in ip_scanner.get_addr()]
        ip_scanner.close()
        subnet = None
        for interface in info:
            if '192.168' in interface['addr']:
                subnet = f'192.168.{interface["addr"].split(".")[2]}'
                return subnet

    def scanner(self, ips_list):
        for ip in ips_list:
            try:
                hostname = socket.gethostbyaddr(ip)[0]
                if 'yeelink' in hostname:
                    self.threads_buffer.append({
                        'hostname': hostname,
                        'ip': ip
                    })
                    logger.info(f'Found new bulb: {hostname} at {ip}')
            except socket.herror as se:
                if 'Unknown host' not in str(se):
                    logger.error(str(se))

    def spawn_scanners(self, ips: list):
        max_threads = self.max_threads
        ips_for_thread = int(len(ips) / max_threads)
        limits = [i * ips_for_thread for i in range(max_threads)]
        ranges = [ips[limit:limit + ips_for_thread + 1] for limit in limits]
        threads = []
        for r in ranges:
            t = threading.Thread(target=self.scanner, args=(r, ))
            t.start()
            threads.append(t)

        t: threading.Thread
        for t in threads:
            t.join()

    def get_bulbs_ips(self):
        logger.info('Scanning network for bulbs...')

        subnet = self.get_subnet()
        if subnet is None:
            logger.error('No router connection! Aborting...')
            return None

        #subnet = '192.168.178'
        logger.debug(f'Subnet: {subnet}')

        ips = [f"{subnet}.{i}" for i in range(0, 256)]

        self.threads_buffer = []

        self.spawn_scanners(ips)

        bulbs = self.threads_buffer

        result = [
            f"hostname: {bulb['hostname']}, ip: {bulb['ip']}" for bulb in bulbs
        ]
        if len(bulbs) > 0:
            logger.info(f'Network scan ended, result:\n' + '\n'.join(result))
        else:
            logger.warning(f'Network scan ended, no bulbs found')

        return bulbs

    def toggle_bulb(self, bulb):
        bulb_obj = Bulb(bulb['ip'])
        response = bulb_obj.toggle()
        if response == 'ok':
            logger.info(f'Toggled {bulb["hostname"]} at {bulb["ip"]}')
        else:
            logger.error(
                f'Toggle error for {bulb["hostname"]} at {bulb["ip"]}')

    def toggle_bulbs(self, bulbs, *args):
        if type(bulbs) is not list:
            available_bulbs = self.bulbs
        else:
            available_bulbs = bulbs
        logger.info('Toggling bulbs...')
        for bulb in available_bulbs:
            self.toggle_bulb(bulb)
        logger.info('All bulbs toggled.')

    def check_mqtt_connection(self):
        logger.debug("Checking mqtt broker connection...")
        if not self.mqtt_client.is_connected():
            logger.warning("Broker connection error, trying reconnection...")
            self.mqtt_client.reconnect()
        if not self.mqtt_client.is_connected():
            logger.error("Reconnection error")

    def loop(self):
        SCAN_RATE = self.scan_rate
        while True:
            try:
                self.bulbs = self.get_bulbs_ips()
                time.sleep(SCAN_RATE)
            except KeyboardInterrupt as ki:
                logger.critical("HUB killed, stopping mqtt loop...")
                try:
                    self.mqtt_client.loop_stop()
                except:
                    self.mqtt_client.loop_stop(force=True)
            except Exception as e:
                logger.critical(f"Unhandled exception: {e}")
                time.sleep(1)

    def turn_off_bulbs(self):
        logger.info("Turning off bulbs...")
        for bulb in self.bulbs:
            bulb_obj = Bulb(bulb['ip'])
            response = bulb_obj.turn_off()
            if response == 'ok':
                logger.info(f'{bulb["hostname"]} turned off at {bulb["ip"]}')
            else:
                logger.error(
                    f'Turn off error for {bulb["hostname"]} at {bulb["ip"]}')

    def startup(self):
        logger.info("Setting up...")
        self.bulbs = self.get_bulbs_ips()
        self.turn_off_bulbs()
Example #10
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._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_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)

    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):
        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
        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_network_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_popup_ip_address)

    def _build_network_status_popup(self):
        return Popup(title='Network Status',
                     content=Label(text='IP ADDRESS WILL GO HERE'),
                     size_hint=(1, 1), auto_dismiss=False)

    def update_popup_ip_address(self, instance):
        interface = "wlan0"
        ipaddr = lampi.lampi_util.get_ip_address(interface)
        deviceid = lampi.lampi_util.get_device_id()
        msg = "{}: {}\nDeviceID: {}\nBroker Bridged: {}".format(
            interface, ipaddr, deviceid, self.mqtt_broker_bridged)
        instance.content.text = msg

    def on_gpio17_pressed(self, instance, value):
        if value:
            self.network_status_popup.open()
        else:
            self.network_status_popup.dismiss()

    def _poll_GPIO(self, dt):
        # GPIO17 is the rightmost button when looking front of LAMPI
        self.gpio17_pressed = not self.pi.read(17)
class 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 #12
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 #13
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 #14
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 #15
0
from paho.mqtt.client import Client

client = Client()
client.username_pw_set('RpiLights', '649t15Db#@4Z')
client.enable_logger()


def topic_callback(client, userdata, message):
    print('topic_callback', message)
    if message.payload.decode('utf-8') == 'on':
        client.publish('topic2', payload='off')


def on_connect(client, userdata, flags, rc):
    print("Connected")
    client.subscribe('topic1')


client.message_callback_add('topic1', topic_callback)
client.on_connect = on_connect

client.connect('cloud.iot-playground.com', port=10048)
print(str(client))

client.subscribe('topic1')
client.publish('topic2', payload=1)

client.loop_forever()