async def _connect_forever(self) -> None: dev_id = hex(getnode()) while True: try: mqtt_connection = await self._mqtt_client.connect( host=self._mqtt_host, port=self._mqtt_port, username=self._mqtt_user, password=self._mqtt_password, client_id=f'ble2mqtt_{dev_id}', will_message=aio_mqtt.PublishableMessage( topic_name=self.availability_topic, payload='offline', qos=aio_mqtt.QOSLevel.QOS_1, retain=True, ), ) _LOGGER.info(f'Connected to {self._mqtt_host}') await self._mqtt_client.publish( aio_mqtt.PublishableMessage( topic_name=self.availability_topic, payload='online', qos=aio_mqtt.QOSLevel.QOS_1, retain=True, ), ) await self._run_device_tasks(mqtt_connection.disconnect_reason) except (aio.CancelledError, KeyboardInterrupt): await self._mqtt_client.publish( aio_mqtt.PublishableMessage( topic_name=self.availability_topic, payload='offline', qos=aio_mqtt.QOSLevel.QOS_0, retain=True, ), ) raise except Exception: _LOGGER.exception( "Connection lost. Will retry in %d seconds.", self._reconnection_interval, ) try: await self.stop_device_manage_tasks() except aio.CancelledError: raise except Exception: _LOGGER.exception('Exception in _connect_forever()') try: await self._mqtt_client.disconnect() except aio.CancelledError: raise except Exception: _LOGGER.error('Disconnect from MQTT broker error') await aio.sleep(self._reconnection_interval)
async def _publish_light(self, light: Light): await self._client.publish( aio_mqtt.PublishableMessage( topic_name=self._get_topic(light.topic), payload=json.dumps(light.state), qos=aio_mqtt.QOSLevel.QOS_1, ), )
async def _connect_forever(self) -> None: while True: try: connect_result = await self._client.connect( host=self._mqtt_host, port=self._mqtt_port, username=self._mqtt_user, password=self._mqtt_password, client_id=f'lumimqtt_{self.dev_id}', will_message=self._will_message, ) logger.info(f"Connected to {self._mqtt_host}") await self._client.publish( aio_mqtt.PublishableMessage( topic_name=self._topic_lwt, payload='online', qos=aio_mqtt.QOSLevel.QOS_1, retain=True, ), ) await self._client.subscribe(*[(t, aio_mqtt.QOSLevel.QOS_1) for t in self.subscribed_topics] ) await self.send_config() logger.info("Wait for network interruptions...") await connect_result.disconnect_reason except aio.CancelledError: raise except aio_mqtt.AccessRefusedError as e: logger.error("Access refused", exc_info=e) raise except ( aio_mqtt.ConnectionLostError, aio_mqtt.ConnectFailedError, aio_mqtt.ServerDiedError, ): logger.error( "Connection lost. Will retry in %d seconds", self._reconnection_interval, ) await aio.sleep(self._reconnection_interval) except aio_mqtt.ConnectionCloseForcedError as e: logger.error("Connection close forced", exc_info=e) return except Exception as e: logger.error( "Unhandled exception during connecting", exc_info=e, ) return else: logger.info("Disconnected") return
async def _publish_sensor(self, sensor: Sensor, value=None): if value is None: value = sensor.get_value() await self._client.publish( aio_mqtt.PublishableMessage( topic_name=self._get_topic(sensor.topic), payload=value, qos=aio_mqtt.QOSLevel.QOS_1, retain=self._sensor_retain, ), )
def __init__( self, device_id: str, topic_root: str, host: str, port: int = None, user: ty.Optional[str] = None, password: ty.Optional[str] = None, reconnection_interval: int = 10, *, auto_discovery: bool, sensor_retain: bool, sensor_threshold: int, sensor_debounce_period: int, light_transition_period: float, light_notification_period: float, loop: ty.Optional[aio.AbstractEventLoop] = None, ) -> None: self.dev_id = device_id self._topic_root = topic_root self._topic_lwt = f'{topic_root}/status' self._mqtt_host = host self._mqtt_port = port self._mqtt_user = user self._mqtt_password = password self._will_message = aio_mqtt.PublishableMessage( topic_name=self._topic_lwt, payload='offline', qos=aio_mqtt.QOSLevel.QOS_1, retain=True, ) self._auto_discovery = auto_discovery self._sensor_retain = sensor_retain self._sensor_threshold = sensor_threshold self._sensor_debounce_period = sensor_debounce_period self._light_transition_period = light_transition_period self._light_notification_period = light_notification_period self._light_last_sent = None self._reconnection_interval = reconnection_interval self._loop = loop or aio.get_event_loop() self._client = aio_mqtt.Client( loop=self._loop, client_id_prefix='lumimqtt_', ) self._tasks: ty.List[aio.Future] = [] self.sensors: ty.List[Sensor] = [] self.lights: ty.List[Light] = [] self.buttons: ty.List[Button] = [] self.custom_commands: ty.List[Command] = [] self._debounce_sensors: ty.Dict[Sensor, DebounceSensor] = {}
async def _read_and_publish(self): dt, co2_ppm, temp = self._read() await self._client.publish( aio_mqtt.PublishableMessage( topic_name=f"{self.mqtt_prefix}/sensor/state", payload=json.dumps({ 'co2': co2_ppm, 'temperature': round(temp, 2), }), qos=aio_mqtt.QOSLevel.QOS_1, ), )
async def _handle_click(self, button: Button): await aio.gather( self._client.publish( aio_mqtt.PublishableMessage( topic_name=self._get_topic(button.topic), payload=json.dumps({'action': 'single'}), qos=aio_mqtt.QOSLevel.QOS_1, ), ), self._client.publish( aio_mqtt.PublishableMessage( topic_name=self._get_topic(f'{button.topic}/action'), payload='single', qos=aio_mqtt.QOSLevel.QOS_1, ), ), ) await self._client.publish( aio_mqtt.PublishableMessage( topic_name=self._get_topic(button.topic), payload=json.dumps({'action': ''}), qos=aio_mqtt.QOSLevel.QOS_1, ), )
async def publish_topic_callback(self, topic, value, nowait=False): _LOGGER.debug(f'call publish callback topic={topic} value={value}') if not self._mqtt_client.is_connected(): _LOGGER.warning(f'{self.device} mqtt is disconnected') return await self._mqtt_client.publish( aio_mqtt.PublishableMessage( topic_name='/'.join((self._base_topic, topic)), payload=value, qos=aio_mqtt.QOSLevel.QOS_1, ), nowait=nowait, )
async def _handle_click(self, button: Button, action: str): logger.debug(f'{button} sent "{action}" event') await aio.gather( self._client.publish( aio_mqtt.PublishableMessage( topic_name=self._get_topic(button.topic), payload=json.dumps({'action': action}), qos=aio_mqtt.QOSLevel.QOS_1, ), ), self._client.publish( aio_mqtt.PublishableMessage( topic_name=self._get_topic(f'{button.topic}/action'), payload=action, qos=aio_mqtt.QOSLevel.QOS_1, ), ), ) await self._client.publish( aio_mqtt.PublishableMessage( topic_name=self._get_topic(button.topic), payload=json.dumps({'action': ''}), qos=aio_mqtt.QOSLevel.QOS_1, ), )
async def _periodic_publish(self, period=1): while True: if not self._client.is_connected(): await aio.sleep(1) continue for sensor in self.sensors: try: value = sensor.get_value() debounce_val = self._debounce_sensors.get(sensor) if self._is_binary(sensor): should_send = (debounce_val is None or value != debounce_val.value) else: should_send = ( debounce_val is None or abs(value - debounce_val.value) >= self._sensor_threshold or (datetime.now() - debounce_val.last_sent).seconds >= self._sensor_debounce_period) if should_send: self._debounce_sensors[sensor] = DebounceSensor( value=value, last_sent=datetime.now(), ) await self._client.publish( aio_mqtt.PublishableMessage( topic_name=self._get_topic(sensor.topic), payload=value, qos=aio_mqtt.QOSLevel.QOS_1, retain=self._sensor_retain, ), ) except ( aio_mqtt.ConnectionClosedError, aio_mqtt.ServerDiedError, ) as e: logger.error("Connection closed", exc_info=e) await self._client.wait_for_connect() continue await aio.sleep(period)
async def _handle_messages(self) -> None: async for message in self._client.delivered_messages( f'{self._topic_root}/#', ): while True: if message.topic_name not in self.subscribed_topics: continue light: ty.Optional[Light] = None for _light in self.lights: if message.topic_name == self._get_topic(_light.topic_set): light = _light if not light: logger.error("Invalid topic for light") break try: value = json.loads(message.payload) except ValueError as e: logger.exception(str(e)) break try: await light.set(value) await self._client.publish( aio_mqtt.PublishableMessage( topic_name=self._get_topic(light.topic), payload=json.dumps(light.state), qos=aio_mqtt.QOSLevel.QOS_1, ), ) except aio_mqtt.ConnectionClosedError as e: logger.error("Connection closed", exc_info=e) await self._client.wait_for_connect() continue except Exception as e: logger.error( "Unhandled exception during echo message publishing", exc_info=e) break
async def send_config(self): device = { 'identifiers': [ f'xiaomi_gateway_{self.dev_id}', ], 'name': f'xiaomi_gateway_{self.dev_id}', 'sw_version': VERSION, 'model': 'Xiaomi Gateway', 'manufacturer': 'Xiaomi', } def get_generic_vals(name): return { 'name': f'{name}_{self.dev_id}', 'unique_id': f'{name}_{self.dev_id}', 'device': device, 'availability_topic': self._topic_lwt, } # set sensors config for sensor in self.sensors: await self._client.publish( aio_mqtt.PublishableMessage( topic_name=( f'homeassistant/' f"{'binary_' if self._is_binary(sensor) else ''}sensor" f'/{self.dev_id}/{sensor.topic}/config'), payload=json.dumps({ **get_generic_vals(sensor.name), **(sensor.MQTT_VALUES or {}), 'state_topic': self._get_topic(sensor.topic), }), qos=aio_mqtt.QOSLevel.QOS_1, retain=True, ), ) # set buttons config for button in self.buttons: base_topic = self._get_topic(button.topic) await aio.gather( self._client.publish( aio_mqtt.PublishableMessage( topic_name=(f'homeassistant/sensor/{self.dev_id}/' f'{button.topic}/config'), payload=json.dumps({ **get_generic_vals(button.name), **(button.MQTT_VALUES or {}), 'json_attributes_topic': base_topic, 'state_topic': base_topic, 'value_template': '{{ value_json.action }}', }), qos=aio_mqtt.QOSLevel.QOS_1, retain=True, ), ), self._client.publish( aio_mqtt.PublishableMessage( topic_name=( f'homeassistant/device_automation/' f'{button.name}_{self.dev_id}/action_single/config' ), payload=json.dumps({ # device_automation should not have # name and unique_id 'device': device, 'automation_type': 'trigger', 'topic': f'{base_topic}/action', 'subtype': 'single', 'payload': 'single', 'type': 'action', }), qos=aio_mqtt.QOSLevel.QOS_1, retain=True, ), ), ) # set LED lights config for light in self.lights: await self._client.publish( aio_mqtt.PublishableMessage( topic_name=f'homeassistant/light/{self.dev_id}/' f'{light.topic}/config', payload=json.dumps({ **get_generic_vals(light.name), 'schema': 'json', 'rgb': light.RGB, 'brightness': light.BRIGHTNESS, 'state_topic': self._get_topic(light.topic), 'command_topic': self._get_topic(light.topic_set), }), qos=aio_mqtt.QOSLevel.QOS_1, retain=True, ), )
async def _connect_forever(self) -> None: while True: try: connect_result = await self._client.connect( host=config.MQTT_SERVER, username=config.MQTT_USER, password=config.MQTT_PASSWORD, ) logger.info(f"Connected to {config.MQTT_SERVER}.") device = { "identifiers": [ self.device_name, ], "name": self.device_name, "sw_version": self._co2.info["serial_no"], "model": self.device_name, "manufacturer": self._co2.info['manufacturer'], } await self._client.publish( aio_mqtt.PublishableMessage( topic_name=f'{self.mqtt_prefix}/co2/config', payload=json.dumps({ 'name': f'co2_{self.device_name}', 'unique_id': f'co2_{self.device_name}', "state_topic": f"{self.mqtt_prefix}/sensor/state", "unit_of_measurement": "ppm", "value_template": "{{ value_json.co2 }}", "device": device, }), qos=aio_mqtt.QOSLevel.QOS_1, retain=True, ), ) await self._client.publish( aio_mqtt.PublishableMessage( topic_name=f'{self.mqtt_prefix}/temperature/config', payload=json.dumps({ "device_class": "temperature", 'name': f'temperature_{self.device_name}', 'unique_id': f'temperature_{self.device_name}', "state_topic": f"{self.mqtt_prefix}/sensor/state", "unit_of_measurement": "\u00b0C", "value_template": "{{ value_json.temperature }}", "device": device, }), qos=aio_mqtt.QOSLevel.QOS_1, retain=True, ), ) logger.info("Wait for network interruptions...") await connect_result.disconnect_reason except asyncio.CancelledError: raise except aio_mqtt.AccessRefusedError: logger.exception("Access refused") except ( aio_mqtt.ConnectionLostError, aio_mqtt.ConnectionClosedError, aio_mqtt.ServerDiedError, OSError, ): logger.exception( "Connection lost. Will retry in %d seconds", self._reconnection_interval, ) await asyncio.sleep(self._reconnection_interval) except aio_mqtt.ConnectionCloseForcedError: logger.error("Connection close forced") for t in self._tasks: t.cancel() return except Exception: logger.exception("Unhandled exception during connecting") else: logger.info("Disconnected") for t in self._tasks: t.cancel() return
async def send_device_config(self): device = self.device device_info = { 'identifiers': [ device.unique_id, ], 'name': device.unique_name, 'model': device.model, } if device.manufacturer: device_info['manufacturer'] = device.manufacturer if device.version: device_info['sw_version'] = device.version def get_generic_vals(entity: dict): name = entity.pop('name') result = { 'name': f'{name}_{device.friendly_id}', 'unique_id': f'{name}_{device.dev_id}', 'device': device_info, 'availability_mode': 'all', 'availability': [ { 'topic': self._global_availability_topic }, { 'topic': '/'.join((self._base_topic, self.device.availability_topic), ) }, ], } icon = entity.pop('icon', None) if icon: result['icon'] = f'mdi:{icon}' entity.pop('topic', None) entity.pop('json', None) entity.pop('main_value', None) result.update(entity) return result messages_to_send = [] for cls, entities in device.entities_with_lqi.items(): if cls in ( BINARY_SENSOR_DOMAIN, SENSOR_DOMAIN, DEVICE_TRACKER_DOMAIN, ): for entity in entities: entity_name = entity['name'] state_topic = self._get_topic( device.unique_id, entity.get('topic', device.STATE_TOPIC), ) config_topic = '/'.join(( CONFIG_MQTT_NAMESPACE, cls, self._config_device_topic, entity_name, 'config', )) if entity.get('json') and entity.get('main_value'): state_topic_part = { 'json_attributes_topic': state_topic, 'state_topic': state_topic, 'value_template': f'{{{{ value_json.{entity["main_value"]} }}}}', } else: state_topic_part = { 'state_topic': state_topic, 'value_template': f'{{{{ value_json.{entity_name} }}}}', } if cls == DEVICE_TRACKER_DOMAIN: state_topic_part['source_type'] = 'bluetooth_le' payload = json.dumps({ **get_generic_vals(entity), **state_topic_part, }) _LOGGER.debug( f'Publish config topic={config_topic}: {payload}', ) messages_to_send.append( aio_mqtt.PublishableMessage( topic_name=config_topic, payload=payload, qos=aio_mqtt.QOSLevel.QOS_1, retain=True, ), ) if cls == SWITCH_DOMAIN: for entity in entities: entity_name = entity['name'] state_topic = self._get_topic( device.unique_id, entity.get('topic', device.STATE_TOPIC), ) command_topic = '/'.join((state_topic, device.SET_POSTFIX)) config_topic = '/'.join(( CONFIG_MQTT_NAMESPACE, cls, self._config_device_topic, entity_name, 'config', )) payload = json.dumps({ **get_generic_vals(entity), 'state_topic': state_topic, 'command_topic': command_topic, }) _LOGGER.debug( f'Publish config topic={config_topic}: {payload}', ) messages_to_send.append( aio_mqtt.PublishableMessage( topic_name=config_topic, payload=payload, qos=aio_mqtt.QOSLevel.QOS_1, retain=True, ), ) # TODO: send real state on receiving status from a device _LOGGER.debug(f'Publish initial state topic={state_topic}') await self._mqtt_client.publish( aio_mqtt.PublishableMessage( topic_name=state_topic, payload='OFF', qos=aio_mqtt.QOSLevel.QOS_1, ), ) if cls == LIGHT_DOMAIN: for entity in entities: entity_name = entity['name'] state_topic = self._get_topic( device.unique_id, entity.get('topic', device.STATE_TOPIC), ) set_topic = '/'.join((state_topic, device.SET_POSTFIX)) config_topic = '/'.join(( CONFIG_MQTT_NAMESPACE, cls, self._config_device_topic, entity_name, 'config', )) payload = json.dumps({ **get_generic_vals(entity), 'schema': 'json', 'color_mode': bool(entity.get('color_mode', True)), 'supported_color_modes': entity.get( 'color_mode', ['rgb'], ), 'brightness': entity.get('brightness', True), 'state_topic': state_topic, 'command_topic': set_topic, }) _LOGGER.debug( f'Publish config topic={config_topic}: {payload}', ) messages_to_send.append( aio_mqtt.PublishableMessage( topic_name=config_topic, payload=payload, qos=aio_mqtt.QOSLevel.QOS_1, retain=True, ), ) if cls == COVER_DOMAIN: for entity in entities: entity_name = entity['name'] state_topic = self._get_topic( device.unique_id, entity.get('topic', device.STATE_TOPIC), ) set_topic = '/'.join((state_topic, device.SET_POSTFIX)) set_position_topic = '/'.join( (state_topic, device.SET_POSITION_POSTFIX), ) config_topic = '/'.join(( CONFIG_MQTT_NAMESPACE, cls, self._config_device_topic, entity_name, 'config', )) config_params = { **get_generic_vals(entity), 'state_topic': state_topic, 'position_topic': state_topic, 'json_attributes_topic': state_topic, 'value_template': '{{ value_json.state }}', 'position_template': '{{ value_json.position }}', 'command_topic': set_topic, 'set_position_topic': set_position_topic, } payload = json.dumps(config_params) _LOGGER.debug( f'Publish config topic={config_topic}: {payload}', ) messages_to_send.append( aio_mqtt.PublishableMessage( topic_name=config_topic, payload=payload, qos=aio_mqtt.QOSLevel.QOS_1, retain=True, ), ) if cls == SELECT_DOMAIN: for entity in entities: entity_name = entity['name'] state_topic = self._get_topic( device.unique_id, entity.get('topic', device.STATE_TOPIC), ) set_topic = '/'.join((state_topic, device.SET_POSTFIX)) config_topic = '/'.join(( CONFIG_MQTT_NAMESPACE, cls, self._config_device_topic, entity_name, 'config', )) config_params = { **get_generic_vals(entity), 'state_topic': state_topic, 'command_topic': set_topic, } payload = json.dumps(config_params) _LOGGER.debug( f'Publish config topic={config_topic}: {payload}', ) messages_to_send.append( aio_mqtt.PublishableMessage( topic_name=config_topic, payload=payload, qos=aio_mqtt.QOSLevel.QOS_1, retain=True, ), ) await aio.gather(*[ self._mqtt_client.publish(message) for message in messages_to_send ]) device.config_sent = True
async def send_config(self): device = { 'identifiers': [ f'xiaomi_gateway_{self.dev_id}', ], 'name': f'xiaomi_gateway_{self.dev_id}', 'sw_version': version, 'model': 'Xiaomi Gateway', 'manufacturer': 'Xiaomi', } def get_generic_vals(name): return { 'name': f'{name}_{self.dev_id}', 'unique_id': f'{name}_{self.dev_id}', 'device': device, 'availability_topic': self._topic_lwt, } # set sensors config for sensor in self.sensors: await self._client.publish( aio_mqtt.PublishableMessage( topic_name=( f'homeassistant/' f"{'binary_' if self._is_binary(sensor) else ''}sensor" f'/{self.dev_id}/{sensor.topic}/config'), payload=json.dumps({ **get_generic_vals(sensor.name), **(sensor.MQTT_VALUES or {}), 'state_topic': self._get_topic(sensor.topic), }), qos=aio_mqtt.QOSLevel.QOS_1, retain=True, ), ) # set buttons config for button in self.buttons: base_topic = self._get_topic(button.topic) messages = [ aio_mqtt.PublishableMessage( topic_name=(f'homeassistant/sensor/{self.dev_id}/' f'{button.topic}/config'), payload=json.dumps({ **get_generic_vals(button.name), **(button.MQTT_VALUES or {}), 'json_attributes_topic': base_topic, 'state_topic': base_topic, 'value_template': '{{ value_json.action }}', }), qos=aio_mqtt.QOSLevel.QOS_1, retain=True, ), ] for event in button.PROVIDE_EVENTS: messages.append( aio_mqtt.PublishableMessage( topic_name=( f'homeassistant/device_automation/' f'{button.name}_{self.dev_id}/action_{event}/config' ), payload=json.dumps({ # device_automation should not have # name and unique_id 'device': device, 'automation_type': 'trigger', 'topic': f'{base_topic}/action', 'subtype': event, 'payload': event, 'type': 'action', }), qos=aio_mqtt.QOSLevel.QOS_1, retain=True, ), ) await aio.gather(*[self._client.publish(m) for m in messages]) # set LED lights config for light in self.lights: await self._client.publish( aio_mqtt.PublishableMessage( topic_name=f'homeassistant/light/{self.dev_id}/' f'{light.topic}/config', payload=json.dumps({ **get_generic_vals(light.name), 'schema': 'json', 'color_mode': True, 'supported_color_modes': [light.COLOR_MODE], 'brightness': light.BRIGHTNESS, 'state_topic': self._get_topic(light.topic), 'command_topic': self._get_topic(light.topic_set), }), qos=aio_mqtt.QOSLevel.QOS_1, retain=True, ), ) for command in self.custom_commands: await self._client.publish( aio_mqtt.PublishableMessage( topic_name=f'homeassistant/switch/' f'{self.dev_id}_{command.name}/config', payload=json.dumps({ **get_generic_vals(command.name), 'state_topic': self._get_topic(command.topic), 'command_topic': self._get_topic(command.topic_set), }), qos=aio_mqtt.QOSLevel.QOS_1, retain=True, ), )
async def _handle_messages(self) -> None: async for message in self._client.delivered_messages( f'{self._topic_root}/#', ): while True: if message.topic_name not in self.subscribed_topics: continue light: ty.Optional[Light] = None for _light in self.lights: if message.topic_name == self._get_topic(_light.topic_set): light = _light if light: try: value = json.loads(message.payload) except ValueError as e: logger.exception(str(e)) break try: await light.set(value, self._light_transition_period) await self._publish_light(light) except aio_mqtt.ConnectionClosedError as e: logger.error("Connection closed", exc_info=e) await self._client.wait_for_connect() continue except Exception as e: logger.error( "Unhandled exception during echo " "message publishing", exc_info=e) break command: ty.Optional[Command] = None for _command in self.custom_commands: if message.topic_name == self._get_topic( _command.topic_set, ): command = _command if command: try: value = json.loads(message.payload) except ValueError: value = message.payload.decode() try: await command.set(value) await self._client.publish( aio_mqtt.PublishableMessage( topic_name=self._get_topic(command.topic), payload='OFF', qos=aio_mqtt.QOSLevel.QOS_1, ), ) except aio_mqtt.ConnectionClosedError as e: logger.error("Connection closed", exc_info=e) await self._client.wait_for_connect() continue except Exception as e: logger.error( "Unhandled exception during echo " "message publishing", exc_info=e, ) break logger.error("Invalid topic for light") break
async def send_message(self, topic, message, retain=False): await self._client.publish(aio_mqtt.PublishableMessage(topic, message, retain=retain))