Profile
Back to NewsBack
GitHub Trending 23 min
Reader Mode
unixorn/ha-mqtt-discoverable: Python module to create MQTT entities that are automatically discovered by Home Assistant

unixorn/ha-mqtt-discoverable: Python module to create MQTT entities that are automatically discovered by Home Assistant

11 hours ago

ha-mqtt-discoverable

License</a> PyPI - Version</a> Ruff</a> GitHub last commit (branch)</a> Downloads</a> Coverage badge</a>

A Python 3 module that takes advantage of Home Assistant's MQTT discovery protocol to create sensors without having to define anything on the HA side.

Using MQTT discoverable devices lets us add new sensors and devices to HA without having to restart HA.

Table of Contents

- Python - Without authentication - With username/password authentication - Using an existing MQTT client - Binary sensor - Button - Camera - Cover - Device - Device trigger - Image - Light - Lock - Number - Select - Sensor - Switch - Text - Valve - Action command valve - Position command valve - I'm having problems on 32-bit ARM platforms - I'm having problems running in systemd-Service - Using UTF-8 field names in Home Assistant UI

Installing

Python

ha-mqtt-discoverable runs on Python 3.10 or later.

pip install ha-mqtt-discoverable if you want to use it in your own Python scripts.

MQTT settings

MQTT broker settings are configured with Settings.MQTT.

Without authentication

For an MQTT broker that does not require authentication, you can specify the host (default: homeassistant). The default MQTT port is 1883 and does not need to be specified but can be overridden when needed:

from ha_mqtt_discoverable import Settings

mqtt_settings = Settings.MQTT( host="localhost", port=1337, )

With username/password authentication

If the MQTT broker requires authentication, provide username and password:

from ha_mqtt_discoverable import Settings

mqtt_settings = Settings.MQTT( host="localhost", username="mqtt-user", password="mqtt-password", )

Using an existing MQTT client

If you already have a configured MQTT client and want to use it, you can pass it directly to Settings.MQTT:

from ha_mqtt_discoverable import Settings
from paho.mqtt.client import Client

Create and configure the MQTT client

client = Client()

Do any additional client configuration here

...

Make sure the client is connected to the broker

client.connect(host="localhost")

Make sure MQTT network communication is running

client.loop_start()

Pass the existing client to ha-mqtt-discoverable.

Other MQTT connection settings are not needed.

mqtt_settings = Settings.MQTT(client=client)

Supported entities

The following Home Assistant entities are currently implemented:

  • Binary sensor
  • Button
  • Camera
  • Cover
  • Device
  • Device trigger
  • Image
  • Light
  • Lock
  • Number
  • Select
  • Sensor
  • Switch
  • Text
  • Valve
Each entity can be associated to a device. See below for details.

Binary sensor

The following example creates a binary sensor and sets its state:

from ha_mqtt_discoverable import Settings
from ha_mqtt_discoverable.sensors import BinarySensor, BinarySensorInfo

Configure the required parameters for the MQTT broker

mqtt_settings = Settings.MQTT(host="localhost")

Information about the sensor

sensor_info = BinarySensorInfo(name="MySensor", device_class="motion")

settings = Settings(mqtt=mqtt_settings, entity=sensor_info)

Instantiate the sensor

mysensor = BinarySensor(settings)

Change the state of the sensor, publishing an MQTT message that gets picked up by HA

mysensor.on() mysensor.off()

Or, change the state using a boolean

mysensor.update_state(True) mysensor.update_state(False)

You can also set custom attributes on the sensor via a Python dict

mysensor.set_attributes({"my attribute": "awesome"})

Button

The button publishes no state, it simply receives a command from HA.

You must call write_config on a Button after creating it to make it discoverable.

from ha_mqtt_discoverable import Settings
from ha_mqtt_discoverable.sensors import Button, ButtonInfo
from paho.mqtt.client import Client, MQTTMessage

Configure the required parameters for the MQTT broker

mqtt_settings = Settings.MQTT(host="localhost")

Information about the button

button_info = ButtonInfo(name="test")

settings = Settings(mqtt=mqtt_settings, entity=button_info)

To receive button commands from HA, define a callback function:

def my_callback(client: Client, user_data, message: MQTTMessage): perform_my_custom_action()

Instantiate the button

my_button = Button(settings, my_callback)

Publish the button's discoverability message to let HA automatically notice it

my_button.write_config()

Camera

The following example creates a camera entity with a topic for the image payload.

from ha_mqtt_discoverable import Settings
from ha_mqtt_discoverable.sensors import Camera, CameraInfo
from paho.mqtt.client import Client, MQTTMessage

Configure the required parameters for the MQTT broker

mqtt_settings = Settings.MQTT(host="localhost")

Information about the camera

camera_info = CameraInfo(name="test", topic="topic_to_publish_image_payload_to")

settings = Settings(mqtt=mqtt_settings, entity=camera_info)

Instantiate the camera

my_camera = Camera(settings)

Set the image payload of the camera

with open("example.png", "rb") as example_file: my_camera.set_payload(example_file.read())

Cover

A cover has five possible states open, closed, opening, closing and stopped. Most other entities use the states as command payload, but covers differentiate on this. The HA user can either open, close or stop it in the cover's current position.

Covers do not currently support tilt.

A callback function is needed in order to parse the commands sent from HA, as the following example shows:

from ha_mqtt_discoverable import Settings
from ha_mqtt_discoverable.sensors import Cover, CoverInfo
from paho.mqtt.client import Client, MQTTMessage

Configure the required parameters for the MQTT broker

mqtt_settings = Settings.MQTT(host="localhost")

Information about the cover

cover_info = CoverInfo(name="test")

settings = Settings(mqtt=mqtt_settings, entity=cover_info)

To receive state commands from HA, define a callback function:

def my_callback(client: Client, user_data, message: MQTTMessage): payload = message.payload.decode() if payload == "OPEN": # let HA know that the cover is opening my_cover.opening() # call function to open cover open_my_custom_cover() # Let HA know that the cover was opened my_cover.open() if payload == "CLOSE": # let HA know that the cover is closing my_cover.closing() # call function to close the cover close_my_custom_cover() # Let HA know that the cover was closed my_cover.closed() if payload == "STOP": # call function to stop the cover stop_my_custom_cover() # Let HA know that the cover was stopped my_cover.stopped()

Instantiate the cover

my_cover = Cover(settings, my_callback)

Set the initial state of the cover, which also makes it discoverable

my_cover.closed()

Device

From the Home Assistant documentation:

A device is a special entity in Home Assistant that is represented by one or more entities.
A device is automatically created when an entity defines its device property. A device will be matched up with an existing device via supplied identifiers or connections, like serial numbers or MAC addresses.

The following example creates a device, by associating multiple sensors to the same DeviceInfo instance.

from ha_mqtt_discoverable import Settings, DeviceInfo
from ha_mqtt_discoverable.sensors import BinarySensor, BinarySensorInfo

Configure the required parameters for the MQTT broker

mqtt_settings = Settings.MQTT(host="localhost")

Define the device. At least one of identifiers or connections must be supplied

device_info = DeviceInfo(name="My device", identifiers="device_id")

Associate the sensor with the device via the device parameter

unique_id must also be set, otherwise Home Assistant will not display the device in the UI

motion_sensor_info = BinarySensorInfo( name="My motion sensor", device_class="motion", unique_id="my_motion_sensor", device=device_info )

motion_settings = Settings(mqtt=mqtt_settings, entity=motion_sensor_info)

Instantiate the sensor

motion_sensor = BinarySensor(motion_settings)

Change the state of the sensor, publishing an MQTT message that gets picked up by HA

motion_sensor.on()

An additional sensor can be added to the same device, by re-using the DeviceInfo instance previously defined

door_sensor_info = BinarySensorInfo(name="My door sensor", device_class="door", unique_id="my_door_sensor", device=device_info) door_settings = Settings(mqtt=mqtt_settings, entity=door_sensor_info)

Instantiate the sensor

door_sensor = BinarySensor(door_settings)

Change the state of the sensor, publishing an MQTT message that gets picked up by HA

door_sensor.on()

The two sensors should be visible inside Home Assistant under the device My device

Device trigger

The following example creates a device trigger and generates a trigger event:

from ha_mqtt_discoverable import DeviceInfo, Settings
from ha_mqtt_discoverable.sensors import DeviceTriggerInfo, DeviceTrigger

Configure the required parameters for the MQTT broker

mqtt_settings = Settings.MQTT(host="localhost")

Define the device. At least one of identifiers or connections must be supplied

device_info = DeviceInfo(name="My device", identifiers="device_id")

Associate the sensor with the device via the device parameter

trigger_info = DeviceTriggerInfo( name="MyTrigger", type="button_press", subtype="button_1", unique_id="my_device_trigger", device=device_info )

settings = Settings(mqtt=mqtt_settings, entity=trigger_info)

Instantiate the device trigger

mytrigger = DeviceTrigger(settings)

Generate a device trigger event, publishing an MQTT message that gets picked up by HA

Optionally include a payload as part of the event

mytrigger.trigger("My custom payload")

Image

The following example creates an image entity to an image url.

from ha_mqtt_discoverable import Settings
from ha_mqtt_discoverable.sensors import Image, ImageInfo

Configure the required parameters for the MQTT broker

mqtt_settings = Settings.MQTT(host="localhost")

Information about the image

image_info = ImageInfo(name="test", url_topic="topic_to_publish_image_url_to") settings = Settings(mqtt=mqtt_settings, entity=image_info)

Instantiate the image

my_image = Image(settings)

Publish an image URL to url_topic

my_image.set_url("http://camera.local/latest.jpg")

The following example creates an image entity and sets the base64 encoded payload.

from ha_mqtt_discoverable import Settings
from ha_mqtt_discoverable.sensors import Image, ImageInfo
from base64 import b64encode

Configure the required parameters for the MQTT broker

mqtt_settings = Settings.MQTT(host="localhost")

Information about the image

image_info = ImageInfo(name="test", image_topic="topic_to_publish_image_payload_to", image_encoding="b64", content_type="image/png") settings = Settings(mqtt=mqtt_settings, entity=image_info)

Instantiate the image

my_image = Image(settings)

Set the image payload

with open("example.png", "rb") as example_file: example_blob = b64encode(example_file.read()) my_image.set_payload(example_blob)

Light

The light is different from the other entities as it needs its payload encoded/decoded as JSON. It is possible to set brightness, effects and the color of the light. Similar to a _switch_ it can also receive 'commands' from HA that request a state change. It is possible to act upon reception of this 'command', by defining a callback function, as the following example shows:

import json
from ha_mqtt_discoverable import Settings
from ha_mqtt_discoverable.sensors import Light, LightInfo
from paho.mqtt.client import Client, MQTTMessage

Configure the required parameters for the MQTT broker

mqtt_settings = Settings.MQTT(host="localhost")

Information about the light

light_info = LightInfo( name="test_light", brightness=True, color_mode=True, supported_color_modes=["rgb"], effect=True, effect_list=["blink", "my_custom_effect"], )

settings = Settings(mqtt=mqtt_settings, entity=light_info)

To receive state commands from HA, define a callback function:

def my_callback(client: Client, user_data, message: MQTTMessage):

# Make sure received payload is JSON try: payload = json.loads(message.payload.decode()) except ValueError: print("Only JSON schema is supported for light entities!") return

# Parse received dictionary if "color" in payload: set_color_of_my_light() my_light.color("rgb", payload["color"]) elif "brightness" in payload: set_brightness_of_my_light() my_light.brightness(payload["brightness"]) elif "effect" in payload: set_effect_of_my_light() my_light.effect(payload["effect"]) elif "state" in payload: if payload["state"] == light_info.payload_on: turn_on_my_light() my_light.on() else: turn_off_my_light() my_light.off() else: print("Unknown payload")

Instantiate the light

my_light = Light(settings, my_callback)

Set the initial state of the light, which also makes it discoverable

my_light.off()

Lock

A lock has five possible states locked, unlocked, locking, unlocking and jammed.

A callback function is needed in order to parse the commands sent from HA, as the following example shows:

from ha_mqtt_discoverable import Settings
from ha_mqtt_discoverable.sensors import Lock, LockInfo
from paho.mqtt.client import Client, MQTTMessage

Configure the required parameters for the MQTT broker

mqtt_settings = Settings.MQTT(host="localhost")

Information about the lock

lock_info = LockInfo(name="test")

settings = Settings(mqtt=mqtt_settings, entity=lock_info)

To receive state commands from HA, define a callback function:

def my_callback(client: Client, user_data, message: MQTTMessage): payload = message.payload.decode() if payload == my_lock._entity.payload_lock: # let HA know that the lock is locking my_lock.locking() # call function to lock the lock lock_my_custom_lock() # Let HA know that the lock is locked now my_lock.locked() if payload == my_lock._entity.payload_unlock: # let HA know that the lock is unlocking my_lock.unlocking() # call function to unlock the lock unlock_my_custom_lock() # Let HA know that the lock is unlocked now my_lock.unlocked()

Instantiate the lock

my_lock = Lock(settings, my_callback)

Set the initial state of the lock, which also makes it discoverable

my_lock.locked()

Number

The number entity is similar to the text entity, but for a numeric value instead of a string. It is possible to act upon receiving changes in HA by defining a callback function, as the following example shows:

import logging
from ha_mqtt_discoverable import Settings
from ha_mqtt_discoverable.sensors import Number, NumberInfo
from paho.mqtt.client import Client, MQTTMessage

Configure the required parameters for the MQTT broker

mqtt_settings = Settings.MQTT(host="localhost")

Information about the number entity.

number_info = NumberInfo(name="test", min=0, max=50, mode="slider", step=5)

settings = Settings(mqtt=mqtt_settings, entity=number_info)

To receive number updates from HA, define a callback function:

def my_callback(client: Client, user_data, message: MQTTMessage): number = int(message.payload.decode()) logging.info(f"Received {number} from HA") do_some_custom_thing(number) # Send an MQTT message to confirm to HA that the number was changed my_number.set_value(number)

Instantiate the number

my_number = Number(settings, my_callback)

Set the initial number displayed in HA UI, publishing an MQTT message that gets picked up by HA

my_number.set_value(42.0)

Select

The selection entity is a list of selectable options in Home Assistant. It is possible to act upon reception of this 'command', by defining a callback function, as the following example shows:

from ha_mqtt_discoverable import Settings
from ha_mqtt_discoverable.sensors import Select, SelectInfo
from paho.mqtt.client import Client, MQTTMessage

Configure the required parameters for the MQTT broker

mqtt_settings = Settings.MQTT(host="localhost")

Information about the select entity

select_info = SelectInfo(name="test", options=["option1", "option2", "option3"])

settings = Settings(mqtt=mqtt_settings, entity=select_info)

To receive state commands from HA, define a callback function:

def my_callback(client: Client, user_data, message: MQTTMessage): payload = message.payload.decode() do_something()

Instantiate the selection

my_selection = Select(settings, my_callback)

Publish the select's discovery message to let HA automatically notice it

my_selection.write_config()

Or select the initial option of the selection, which also makes it discoverable

my_selection.select_option("option1")

Sensor

The following example creates a sensor and sets its state:

from ha_mqtt_discoverable import Settings
from ha_mqtt_discoverable.sensors import Sensor, SensorInfo

Configure the required parameters for the MQTT broker

mqtt_settings = Settings.MQTT(host="localhost")

Information about the sensor

sensor_info = SensorInfo( name="MyTemperatureSensor", device_class="temperature", unit_of_measurement="°C", )

settings = Settings(mqtt=mqtt_settings, entity=sensor_info)

Instantiate the sensor

mysensor = Sensor(settings)

Change the state of the sensor, publishing an MQTT message that gets picked up by HA

mysensor.set_state(20.5)

Switch

The switch is similar to a _binary sensor_, but in addition to publishing state changes toward HA it can also receive 'commands' from HA that request a state change. It is possible to act upon reception of this 'command', by defining a callback function, as the following example shows:

from ha_mqtt_discoverable import Settings
from ha_mqtt_discoverable.sensors import Switch, SwitchInfo
from paho.mqtt.client import Client, MQTTMessage

Configure the required parameters for the MQTT broker

mqtt_settings = Settings.MQTT(host="localhost")

Information about the switch

switch_info = SwitchInfo(name="test")

settings = Settings(mqtt=mqtt_settings, entity=switch_info)

To receive state commands from HA, define a callback function:

def my_callback(client: Client, user_data, message: MQTTMessage): payload = message.payload.decode() if payload == "ON": turn_my_custom_thing_on() # Let HA know that the switch was successfully activated my_switch.on() elif payload == "OFF": turn_my_custom_thing_off() # Let HA know that the switch was successfully deactivated my_switch.off()

Instantiate the switch

my_switch = Switch(settings, my_callback)

Set the initial state of the switch, which also makes it discoverable

my_switch.off()

Text

The text is a 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:

import logging
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=text_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") do_some_custom_thing(text) # Send an MQTT message to confirm to HA that the text was changed my_text.set_text(text)

Instantiate the text

my_text = Text(settings, my_callback)

Set the initial text displayed in HA UI, publishing an MQTT message that gets picked up by HA

my_text.set_text("Some awesome text")

Valve

The valve has two control modes: action command and position command.

Action command valve

This mode is active when reports_position = False. A valve in this mode can report five possible states open, closed, opening, closing and stopped. The command payload can be either OPEN, CLOSE or STOP, where STOP is optional.

A callback function is needed in order to parse the commands sent from HA, as the example shows:

from ha_mqtt_discoverable import Settings
from ha_mqtt_discoverable.sensors import Valve, ValveInfo
from paho.mqtt.client import Client, MQTTMessage

Configure the required parameters for the MQTT broker

mqtt_settings = Settings.MQTT(host="localhost")

Information about the valve

valve_info = ValveInfo(name="test")

settings = Settings(mqtt=mqtt_settings, entity=valve_info)

To receive state commands from HA, define a callback function:

def my_callback(client: Client, user_data, message: MQTTMessage): payload = message.payload.decode() if payload == my_valve._entity.payload_open: # let HA know that the valve is opening my_valve.opening() # call function to open valve open_my_custom_valve() # Let HA know that the valve was opened my_valve.open() elif payload == my_valve._entity.payload_close: # let HA know that the valve is closing my_valve.closing() # call function to close valve close_my_custom_valve() # Let HA know that the valve was closed my_valve.closed() # if payload_stop is defined, stop a valve in motion elif payload == my_valve._entity.payload_stop: # call function to stop the valve. stop_my_custom_valve() # There is no my_valve.stopped(). This means # the function should call my_valve.open() or # my_valve.closed() depending on the valve # state. Otherwise the former state remains # active, this could thus be opening or # closing.

Instantiate the valve

my_valve = Valve(settings, my_callback)

Set the initial state of the valve, which also makes it discoverable

my_valve.closed()

Position command valve

This mode is active when reports_position = True. The mode is different from other sensors as it can send a payload encoded/decoded as JSON, like the light entity can.

The valve can report two possible states opening and closing, a position (0-100) or a combination in JSON format, for example: '{"state": "opening", "position": 42}'. The command payload can be either STOP or a position (0-100).

Note that in this position mode, payload_open, payload_close, state_open and state_closed must be set to None.

A callback function is needed in order to parse the commands sent from HA, as the following example shows:

from ha_mqtt_discoverable import Settings
from ha_mqtt_discoverable.sensors import Valve, ValveInfo
from paho.mqtt.client import Client, MQTTMessage

Configure the required parameters for the MQTT broker

mqtt_settings = Settings.MQTT(host="localhost")

Information about the valve

valve_info = ValveInfo( name="test-position", reports_position=True, payload_open=None, payload_close=None, state_open=None, state_closed=None, )

settings = Settings(mqtt=mqtt_settings, entity=valve_info)

current_position = 0

To receive state commands from HA, define a callback function:

def my_callback(client: Client, user_data, message: MQTTMessage): payload = message.payload.decode() # convert the payload of type str to int try: payload = int(payload) except: logger.error("Wrong payload") return

if payload < my_valve.last_state: # let HA know that the valve is closing my_valve.position(my_valve.last_state, my_valve._entity.state_closing) else: # let HA know that the valve is opening my_valve.position(my_valve.last_state, my_valve._entity.state_opening) # call function to move the valve to the desired position move_my_custom_valve(payload) # Let HA know that the valve reached the desired position. # In HA the positions 0 or 100 automatically show as closed # or open respectively. # Intermediate positions return open with Home Assistant 2026.6 and newer # (fixed in https://github.com/home-assistant/core/pull/165176). my_valve.position(payload)

Instantiate the valve

my_valve = Valve(settings, my_callback)

Set the initial position of the valve, which also makes it discoverable

my_valve.position(0)

Availability Management

[!WARNING]
This feature is not supported if using an existing MQTT client.

If manual_availability is set to True:

  • set_availability has to be called to indicate if an entity is _available_ or _unavailable_
  • a retained Last Will and Testament (LWT) is set, marking the entity _unavailable_ in case the MQTT client disconnects unexpectedly
from ha_mqtt_discoverable import Settings
from ha_mqtt_discoverable.sensors import Sensor, SensorInfo

mqtt_settings = Settings.MQTT(host="localhost")

sensor_info = SensorInfo( name="Power", unit_of_measurement="W", device_class="power", state_class="measurement", ) settings = Settings(mqtt=mqtt_settings, entity=sensor_info, manual_availability=True) sensor = Sensor(settings)

When the entity is ready, set availability to 'True'

sensor.set_availability(True) sensor.set_state(1337)

Limited state changes publications

By default, the state of an entity is only published if it changed compared to its previous state.

If the entity is supposed to publish its state in any case, use the force_update property of the respective method indicating a state update and set it to True.

from ha_mqtt_discoverable import Settings
from ha_mqtt_discoverable.sensors import Sensor, SensorInfo

Configure the required parameters for the MQTT broker

mqtt_settings = Settings.MQTT(host="localhost")

Information about the sensor

sensor_info = SensorInfo( name="MyTemperatureSensor", device_class="temperature", unit_of_measurement="°C", )

settings = Settings(mqtt=mqtt_settings, entity=sensor_info)

Instantiate the sensor

mysensor = Sensor(settings)

Change the state of the sensor, publishing an MQTT message that gets picked up by HA

mysensor.set_state(20.5)

State will not be published as it is the same as before

mysensor.set_state(20.5)

State will be published - it is the same as before but the update is forced

mysensor.set_state(20.5, force_update=True)

FAQ

I'm having problems on 32-bit ARM platforms

ha-mqtt-discoverable depends on pydantic V2. pydantic V2 depends on pydantic_core - the core validation logic for pydantic V2 written in Rust

pydantic_core wheels might not be available for your 32-bit ARM platform:

  • ARMv7 is officially supported -
  • ARMv6 is not officially supported yet -
If you are using any of the Raspberry Pi models together with Debian, piwheels offers pydantic_core wheels for ARMv6 and ARMv7.

I'm having problems running in systemd-Service

Each entity creates its own thread for the MQTT-client-loop, which increases the task count. systemd may limit the tasks to a too low number for your needs (check with systemctl status your.service), which may lead to new entities failing to create a worker-thread. Try setting TasksMax= to an appropriate high number accommodating your entity count and other threads that may spawn.

Alternatively use an existing MQTT client without each entity generating their own MQTT-client-loop.

Using UTF-8 field names in Home Assistant UI

The _name_ field of an entity only supports ASCII characters, if you want to use UTF-8 characters in the Home Assistant UI, use the _display_name_ field (_name_ is then only used under the hood, e.g. as part of the MQTT topic name). For example:

sensor_info = SensorInfo(
    name="Power",
    display_name="Power(功率传感器)",
    unit_of_measurement="W",
    device_class="power",
    state_class="measurement",
)

Contributing

Please run ruff on your code before submitting. There are git hooks already configured to run ruff and other checks before every commit. Please run pre-commit install to enable them.

Users of ha-mqtt-discoverable

If you use this module for your own project, please add a link here.

  • plejd-mqtt-ha - A containerized Python application that bridges Plejd devices to Home Assistant
  • yahac - Yet Another Home Assistant Client
  • PCTools - Link your Windows computer to Home Assistant

Contributors

Contributors</a>

Made with contributors-img.

Chat with me