diff --git a/README.md b/README.md index 34481e4..c8806d1 100644 --- a/README.md +++ b/README.md @@ -20,10 +20,12 @@ Using MQTT discoverable devices lets us add new sensors and devices to HA withou - [Usage](#usage) - [Switch](#switch) - [Usage](#usage-1) + - [Text](#text) + - [Usage](#usage-2) - [Device](#device) - - [Usage](#usage-2) + - [Usage](#usage-3) - [Device trigger](#device-trigger) - - [Usage](#usage-3) + - [Usage](#usage-4) - [Contributing](#contributing) - [Users of ha-mqtt-discoverable](#users-of-ha-mqtt-discoverable) - [Contributors](#contributors) @@ -48,6 +50,8 @@ The following Home Assistant entities are currently implemented: - Button - Device trigger +Each entity can associated to a device. See below for details. + ### Binary sensor #### Usage @@ -95,8 +99,7 @@ from paho.mqtt.client import Client, MQTTMessage mqtt_settings = Settings.MQTT(host="localhost") # Information about the switch -# If `command_topic` is defined, it will receive state updates from HA -switch_info = SwitchInfo(name="test", command_topic="command") +switch_info = SwitchInfo(name="test") settings = Settings(mqtt=mqtt_settings, entity=switch_info) @@ -118,6 +121,43 @@ my_switch.off() ``` +### Text + +The text is an `helper entity`, showing an input field in the HA UI that the user can interact with. +It is possible to act upon reception of the inputted text by defining a `callback` function, as the following example shows: + +#### Usage + +```py +from ha_mqtt_discoverable import Settings +from ha_mqtt_discoverable.sensors import Text, TextInfo +from paho.mqtt.client import Client, MQTTMessage + +# Configure the required parameters for the MQTT broker +mqtt_settings = Settings.MQTT(host="localhost") + +# Information about the `text` entity +text_info = TextInfo(name="test") + +settings = Settings(mqtt=mqtt_settings, entity=switch_info) + +# To receive text updates from HA, define a callback function: +def my_callback(client: Client, user_data, message: MQTTMessage): + text = message.payload.decode() + logging.info(f"Received {text} from HA") + # Your custom code... + +# Define an optional object to be passed back to the callback +user_data = "Some custom data" + +# Instantiate the text +my_text = Text(settings, my_callback, user_data) + +# Change the text displayed in HA UI, publishing an MQTT message that gets picked up by HA +my_text.set_text("Some awesome text") + +``` + ## Device From the [Home Assistant documentation](https://developers.home-assistant.io/docs/device_registry_index): > A device is a special entity in Home Assistant that is represented by one or more entities. diff --git a/ha_mqtt_discoverable/__init__.py b/ha_mqtt_discoverable/__init__.py index 40d88bf..9b5a5cb 100644 --- a/ha_mqtt_discoverable/__init__.py +++ b/ha_mqtt_discoverable/__init__.py @@ -504,6 +504,8 @@ class EntityInfo(BaseModel): """Sets the class of the device, changing the device state and icon that is displayed on the frontend.""" enabled_by_default: Optional[bool] = None """Flag which defines if the entity should be enabled when first added.""" + entity_category: Optional[str] = None + """Classification of a non-primary entity.""" expire_after: Optional[int] = None """If set, it defines the number of seconds after the sensor’s state expires, if it’s not updated.\ After expiry, the sensor’s state becomes unavailable. Default the sensors state never expires.""" diff --git a/ha_mqtt_discoverable/sensors.py b/ha_mqtt_discoverable/sensors.py index f4736a2..982837e 100644 --- a/ha_mqtt_discoverable/sensors.py +++ b/ha_mqtt_discoverable/sensors.py @@ -81,6 +81,24 @@ class ButtonInfo(EntityInfo): """If the published message should have the retain flag on or not""" +class TextInfo(EntityInfo): + """Information about the `text` entity""" + + component: str = "text" + + max: int = 255 + """The maximum size of a text being set or received (maximum is 255).""" + min: int = 0 + """The minimum size of a text being set or received.""" + mode: Optional[str] = "text" + """The mode off the text entity. Must be either text or password.""" + pattern: Optional[str] = None + """A valid regular expression the text being set or received must match with.""" + + retain: Optional[bool] = None + """If the published message should have the retain flag on or not""" + + class DeviceTriggerInfo(EntityInfo): """Information about the device trigger""" @@ -190,3 +208,24 @@ def trigger(self, payload: Optional[str] = None): """ return self._state_helper(payload, self.state_topic, retain=False) + + +class Text(Subscriber[TextInfo]): + """Implements an MQTT text: + https://www.home-assistant.io/integrations/text.mqtt/ + """ + + def set_text(self, text: str) -> None: + """ + Update the text displayed by this sensor. Check that it is of acceptable length. + + Args: + text(str): Value of the text configured for this entity + """ + if not self._entity.min <= len(text) <= self._entity.max: + raise RuntimeError( + f"Text is not within configured length boundaries [{self._entity.min}, {self._entity.max}]" + ) + + logger.info(f"Setting {self._entity.name} to {text} using {self.state_topic}") + self._state_helper(str(text)) diff --git a/tests/test_text.py b/tests/test_text.py new file mode 100644 index 0000000..c404667 --- /dev/null +++ b/tests/test_text.py @@ -0,0 +1,40 @@ +import random +import string +import pytest +from ha_mqtt_discoverable import Settings +from ha_mqtt_discoverable.sensors import Text, TextInfo + + +@pytest.fixture() +def text() -> Text: + mqtt_settings = Settings.MQTT(host="localhost") + text_info = TextInfo(name="test", min=5) + settings = Settings(mqtt=mqtt_settings, entity=text_info) + # Define empty callback + return Text(settings, lambda *_: None) + + +def test_required_config(): + mqtt_settings = Settings.MQTT(host="localhost") + text_info = TextInfo(name="test") + settings = Settings(mqtt=mqtt_settings, entity=text_info) + # Define empty callback + text = Text(settings, lambda *_: None) + assert text is not None + + +def test_set_text(text: Text): + text.set_text("this is as test") + + +def test_too_short_string(text: Text): + with pytest.raises(RuntimeError): + text.set_text("t") + + +def test_too_long_string(text: Text): + length = 500 + letters = string.ascii_lowercase + random_string = "".join(random.choice(letters) for i in range(length)) + with pytest.raises(RuntimeError): + text.set_text(random_string)