Skip to content

Commit

Permalink
Create bound callback_message_received method for handling mqtt callb…
Browse files Browse the repository at this point in the history
…acks (#117951)

* Create bound callback_message_received method for handling mqtt callbacks

* refactor a bit

* fix ruff

* reduce overhead

* cleanup

* cleanup

* Revert changes alarm_control_panel

* Add sensor and binary sensor

* use same pattern for MqttAttributes/MqttAvailability

* remove useless function since we did not need to add to it

* code cleanup

* collapse

---------

Co-authored-by: J. Nick Koston <nick@koston.org>
  • Loading branch information
jbouwh and bdraco authored May 24, 2024
1 parent d4df86d commit 9333965
Show file tree
Hide file tree
Showing 5 changed files with 206 additions and 148 deletions.
158 changes: 78 additions & 80 deletions homeassistant/components/mqtt/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from __future__ import annotations

from datetime import datetime, timedelta
from functools import partial
import logging
from typing import Any

Expand Down Expand Up @@ -37,13 +38,7 @@
from . import subscription
from .config import MQTT_RO_SCHEMA
from .const import CONF_ENCODING, CONF_QOS, CONF_STATE_TOPIC, PAYLOAD_NONE
from .debug_info import log_messages
from .mixins import (
MqttAvailability,
MqttEntity,
async_setup_entity_entry_helper,
write_state_on_attr_change,
)
from .mixins import MqttAvailability, MqttEntity, async_setup_entity_entry_helper
from .models import MqttValueTemplate, ReceiveMessage
from .schemas import MQTT_ENTITY_COMMON_SCHEMA

Expand Down Expand Up @@ -162,92 +157,95 @@ def _setup_from_config(self, config: ConfigType) -> None:
entity=self,
).async_render_with_possible_json_value

def _prepare_subscribe_topics(self) -> None:
"""(Re)Subscribe to topics."""
@callback
def _off_delay_listener(self, now: datetime) -> None:
"""Switch device off after a delay."""
self._delay_listener = None
self._attr_is_on = False
self.async_write_ha_state()

@callback
def off_delay_listener(now: datetime) -> None:
"""Switch device off after a delay."""
self._delay_listener = None
self._attr_is_on = False
self.async_write_ha_state()

@callback
@log_messages(self.hass, self.entity_id)
@write_state_on_attr_change(self, {"_attr_is_on", "_expired"})
def state_message_received(msg: ReceiveMessage) -> None:
"""Handle a new received MQTT state message."""
# auto-expire enabled?
if self._expire_after:
# When expire_after is set, and we receive a message, assume device is
# not expired since it has to be to receive the message
self._expired = False

# Reset old trigger
if self._expiration_trigger:
self._expiration_trigger()

# Set new trigger
self._expiration_trigger = async_call_later(
self.hass, self._expire_after, self._value_is_expired
)
def _state_message_received(self, msg: ReceiveMessage) -> None:
"""Handle a new received MQTT state message."""

payload = self._value_template(msg.payload)
if not payload.strip(): # No output from template, ignore
_LOGGER.debug(
(
"Empty template output for entity: %s with state topic: %s."
" Payload: '%s', with value template '%s'"
),
self.entity_id,
self._config[CONF_STATE_TOPIC],
msg.payload,
self._config.get(CONF_VALUE_TEMPLATE),
)
return
# auto-expire enabled?
if self._expire_after:
# When expire_after is set, and we receive a message, assume device is
# not expired since it has to be to receive the message
self._expired = False

if payload == self._config[CONF_PAYLOAD_ON]:
self._attr_is_on = True
elif payload == self._config[CONF_PAYLOAD_OFF]:
self._attr_is_on = False
elif payload == PAYLOAD_NONE:
self._attr_is_on = None
else: # Payload is not for this entity
template_info = ""
if self._config.get(CONF_VALUE_TEMPLATE) is not None:
template_info = (
f", template output: '{payload!s}', with value template"
f" '{self._config.get(CONF_VALUE_TEMPLATE)!s}'"
)
_LOGGER.info(
(
"No matching payload found for entity: %s with state topic: %s."
" Payload: '%s'%s"
),
self.entity_id,
self._config[CONF_STATE_TOPIC],
msg.payload,
template_info,
)
return
# Reset old trigger
if self._expiration_trigger:
self._expiration_trigger()

if self._delay_listener is not None:
self._delay_listener()
self._delay_listener = None
# Set new trigger
self._expiration_trigger = async_call_later(
self.hass, self._expire_after, self._value_is_expired
)

off_delay: int | None = self._config.get(CONF_OFF_DELAY)
if self._attr_is_on and off_delay is not None:
self._delay_listener = evt.async_call_later(
self.hass, off_delay, off_delay_listener
payload = self._value_template(msg.payload)
if not payload.strip(): # No output from template, ignore
_LOGGER.debug(
(
"Empty template output for entity: %s with state topic: %s."
" Payload: '%s', with value template '%s'"
),
self.entity_id,
self._config[CONF_STATE_TOPIC],
msg.payload,
self._config.get(CONF_VALUE_TEMPLATE),
)
return

if payload == self._config[CONF_PAYLOAD_ON]:
self._attr_is_on = True
elif payload == self._config[CONF_PAYLOAD_OFF]:
self._attr_is_on = False
elif payload == PAYLOAD_NONE:
self._attr_is_on = None
else: # Payload is not for this entity
template_info = ""
if self._config.get(CONF_VALUE_TEMPLATE) is not None:
template_info = (
f", template output: '{payload!s}', with value template"
f" '{self._config.get(CONF_VALUE_TEMPLATE)!s}'"
)
_LOGGER.info(
(
"No matching payload found for entity: %s with state topic: %s."
" Payload: '%s'%s"
),
self.entity_id,
self._config[CONF_STATE_TOPIC],
msg.payload,
template_info,
)
return

if self._delay_listener is not None:
self._delay_listener()
self._delay_listener = None

off_delay: int | None = self._config.get(CONF_OFF_DELAY)
if self._attr_is_on and off_delay is not None:
self._delay_listener = evt.async_call_later(
self.hass, off_delay, self._off_delay_listener
)

def _prepare_subscribe_topics(self) -> None:
"""(Re)Subscribe to topics."""

self._sub_state = subscription.async_prepare_subscribe_topics(
self.hass,
self._sub_state,
{
"state_topic": {
"topic": self._config[CONF_STATE_TOPIC],
"msg_callback": state_message_received,
"msg_callback": partial(
self._message_callback,
self._state_message_received,
{"_attr_is_on", "_expired"},
),
"entity_id": self.entity_id,
"qos": self._config[CONF_QOS],
"encoding": self._config[CONF_ENCODING] or None,
}
Expand Down
10 changes: 8 additions & 2 deletions homeassistant/components/mqtt/debug_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,12 @@ def add_subscription(
hass: HomeAssistant,
message_callback: MessageCallbackType,
subscription: str,
entity_id: str | None = None,
) -> None:
"""Prepare debug data for subscription."""
if entity_id := getattr(message_callback, "__entity_id", None):
if not entity_id:
entity_id = getattr(message_callback, "__entity_id", None)
if entity_id:
entity_info = hass.data[DATA_MQTT].debug_info_entities.setdefault(
entity_id, {"subscriptions": {}, "discovery_data": {}, "transmitted": {}}
)
Expand All @@ -104,9 +107,12 @@ def remove_subscription(
hass: HomeAssistant,
message_callback: MessageCallbackType,
subscription: str,
entity_id: str | None = None,
) -> None:
"""Remove debug data for subscription if it exists."""
if (entity_id := getattr(message_callback, "__entity_id", None)) and entity_id in (
if not entity_id:
entity_id = getattr(message_callback, "__entity_id", None)
if entity_id and entity_id in (
debug_info_entities := hass.data[DATA_MQTT].debug_info_entities
):
debug_info_entities[entity_id]["subscriptions"][subscription]["count"] -= 1
Expand Down
Loading

0 comments on commit 9333965

Please sign in to comment.