├── LICENSE ├── README.md └── ha_mqtt_device.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Stefan Agner 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MicroPython library to create sensors for HomeAssistant using MQTT Discovery 2 | 3 | This is a simple MicroPython library which allows to create sensors and group of 4 | sensors for HomeAssistant. The library makes use of the HomeAssistant [MQTT 5 | Discovery](https://www.home-assistant.io/docs/mqtt/discovery/) method by 6 | announcing the devices through a configuration topic. 7 | 8 | ## Getting Started 9 | 10 | ### Prerequisites 11 | 12 | * MicroPython board capable of sending MQTT messages 13 | * HomeAssistant with MQTT Broker running 14 | * Some kind of sensor (not mandatory, but useful ;-)) 15 | 16 | ### Usage 17 | 18 | #### MQTT Client 19 | 20 | First a MQTT Client needs to be created. The module 21 | [umqtt.simple](https://github.com/micropython/micropython-lib/tree/master/umqtt.simple) 22 | provides a simple MQTT client. The module can be installed using upip. Make 23 | sure you are connected to the internet, e.g. using WiFi: 24 | 25 | ``` 26 | import network 27 | wl = network.WLAN(network.STA_IF) 28 | wl.active(True) 29 | wl.connect(, ) 30 | wl.config(dhcp_hostname="Testboard") 31 | ``` 32 | 33 | Install the `umqtt.simple` module and create a MQTTClient object: 34 | ``` 35 | upip.install("micropython-umqtt.simple") 36 | from umqtt.simple import MQTTClient 37 | mqttc = MQTTClient(b"umqtt-testboard", , keepalive=10) 38 | mqttc.connect() 39 | ``` 40 | 41 | #### Basic sensor 42 | 43 | This example creates a single sensor and announces values. The second argument 44 | to the `Sensor` constructor is the name argument and will be the entity name in 45 | HomeAssistant. The argument `extra_config` allows to pass additional 46 | configuration values as specified by the [MQTT Sensor 47 | Component](https://www.home-assistant.io/integrations/sensor.mqtt/). In this 48 | case the device class and unit of measurement of the sensor is specified. 49 | 50 | With the creation of the Sensor object a persistent MQTT message is sent to the 51 | discovery topic. The configuration will be picked up by HomeAssistant and a 52 | sensor is created. The topic for the entites states update is handled by the 53 | class. 54 | 55 | The `publish_state()` function will send the MQTT message to the state topic 56 | which will update the state of the particular entity in HomeAssistant. 57 | 58 | ``` 59 | # Temperature sensor... 60 | import time 61 | temp_config = { "unit_of_measurement": "°C", "device_class": "temperature" } 62 | temp = Sensor(mqttc, b"temperature_sensor", b"sensorid", extra_conf=temp_config) 63 | for i in range(10, 30): 64 | temp.publish_state(str(i)) 65 | time.sleep(1) 66 | 67 | # This deletes the sensor from HomeAssistant as well! 68 | temp.remove_entity() 69 | ``` 70 | 71 | #### Multiple sensors 72 | 73 | Multiple entities can share the same MQTT state topic. This allows to send a 74 | single (JSON formatted) MQTT message to update multiple entites in 75 | HomeAssistant. For this each entity needs to have a `value_template` specified 76 | so each entity know which value it needs to parse. 77 | 78 | ``` 79 | group = EntityGroup(mqttc, b"testboard") 80 | sensor1_config = { "unit_of_measurement": "°C", "device_class": "temperature", 81 | "value_template": "{{ value_json.temperature }}" } 82 | sensor1 = group.create_sensor(b"test1", b"test1id", extra_conf=sensor1_config) 83 | 84 | sensor2_config = { "unit_of_measurement": "%", "device_class": "humidity", 85 | "value_template": "{{ value_json.humidity }}" } 86 | sensor2 = group.create_sensor(b"test2", b"test2id", extra_conf=sensor2_config) 87 | ``` 88 | 89 | To update the group the `publish_state()` function on the group object needs to 90 | be used: 91 | ``` 92 | for i in range(10, 30): 93 | group.publish_state({ "temperature": str(i), "humidity": str(10 + i) }) 94 | time.sleep(1) 95 | ``` 96 | 97 | To delete all entities of this group: 98 | ``` 99 | group.remove_group() 100 | ``` 101 | 102 | #### Multiple sensors with Device registry 103 | 104 | HomeAssistant allows to group entites into devices via the [Device 105 | Registry](https://developers.home-assistant.io/docs/device_registry_index/) 106 | concept. MQTT entities allow to use the [device 107 | registry](https://www.home-assistant.io/integrations/sensor.mqtt/#device) to 108 | group individual entities to a device as well. 109 | 110 | For this to work a `device` dictionary needs to be added to the configuration 111 | variable. A `unique_id` property with a unique ID for each entity is required 112 | too. 113 | 114 | ``` 115 | device_conf = { "identifiers": [ "common_identifier" ], "name": "Testboard", 116 | "manufacturer": "MicroPython", "model": "TinyPico", "sw_version": "0.1" } 117 | common_conf = { "device": device_conf } 118 | group = EntityGroup(mqttc, b"testboarder", extra_conf=common_conf) 119 | 120 | sensor1_config = { "unit_of_measurement": "°C", "device_class": 121 | "temperature", "value_template": "{{ value_json.temperature }}"), 122 | "unique_id": mqtt_status + "sensor1"} 123 | sensor1 = group.create_sensor(b"test1", b"test1id", extra_conf=sensor1_config) 124 | 125 | sensor2_config = { "unit_of_measurement": "%", "device_class": 126 | "humidity", "value_template": "{{ value_json.humidity }}", 127 | "unique_id": mqtt_status + "sensor2"} 128 | sensor2 = group.create_sensor(b"test2", b"test2id", extra_conf=sensor2_config) 129 | ``` 130 | 131 | Publishing state and removing the group stays the same as regular entity 132 | groups. 133 | 134 | #### Using availability topic 135 | 136 | MQTT allows to specify a last will message which is sent to the specified topic 137 | when a device does not connect for a certain period. The last will message is 138 | transmitted when connecting, hence this needs to be configured before 139 | connecting. Also choose a keep alive timeout which is higher than your expected 140 | sensor update time. E.g. if you plan to update the sensor value every minute, 141 | a value higher than 60 seconds make sense. The same topic then needs to be 142 | specified when creating the sensor: 143 | 144 | ``` 145 | mqtt_availability_topic = b"testboard/status" 146 | mqttc = MQTTClient(b"umqtt-testboard", , keepalive=70) 147 | mqttc.set_last_will(mqtt_status, b"offline", True) 148 | mqttc.connect() 149 | mqttc.publish(mqtt_status, b"online", retain=True) 150 | 151 | temp_config = { "availability_topic": mqtt_availability_topic } 152 | temp = Sensor(mqttc, b"temperature_sensor", b"sensorid", extra_conf=temp_config) 153 | ``` 154 | 155 | ## License 156 | 157 | This project is licensed under the MIT License - see the 158 | [LICENSE](LICENSE) file for details 159 | 160 | -------------------------------------------------------------------------------- /ha_mqtt_device.py: -------------------------------------------------------------------------------- 1 | import ujson as json 2 | 3 | class BaseEntity(object): 4 | 5 | def __init__(self, mqtt, name, component, object_id, node_id, discovery_prefix, extra_conf): 6 | self.mqtt = mqtt 7 | 8 | base_topic = discovery_prefix + b'/' + component + b'/' 9 | if node_id: 10 | base_topic += node_id + b'/' 11 | base_topic += object_id + b'/' 12 | 13 | self.config_topic = base_topic + b'config' 14 | self.state_topic = base_topic + b'state' 15 | 16 | self.config = {"name": name, "state_topic": self.state_topic} 17 | if extra_conf: 18 | self.config.update(extra_conf) 19 | self.mqtt.publish(self.config_topic, bytes(json.dumps(self.config), 'utf-8'), True, 1) 20 | 21 | def remove_entity(self): 22 | self.mqtt.publish(self.config_topic, b'', 1) 23 | 24 | def publish_state(self, state): 25 | self.mqtt.publish(self.state_topic, state) 26 | 27 | class BinarySensor(BaseEntity): 28 | 29 | def __init__(self, mqtt, name, object_id, node_id=None, 30 | discovery_prefix=b'homeassistant', extra_conf=None): 31 | 32 | super().__init__(mqtt, name, b'binary_sensor', object_id, node_id, 33 | discovery_prefix, extra_conf) 34 | 35 | def publish_state(self, state): 36 | self.mqtt.publish(self.state_topic, b'ON' if state else b'OFF') 37 | 38 | def on(self): 39 | self.publish_state(True) 40 | 41 | def off(self): 42 | self.publish_state(False) 43 | 44 | class Sensor(BaseEntity): 45 | 46 | def __init__(self, mqtt, name, object_id, node_id=None, 47 | discovery_prefix=b'homeassistant', extra_conf=None): 48 | 49 | super().__init__(mqtt, name, b'sensor', object_id, node_id, 50 | discovery_prefix, extra_conf) 51 | 52 | class EntityGroup(object): 53 | 54 | def __init__(self, mqtt, node_id, discovery_prefix=b'homeassistant', 55 | extra_conf=None): 56 | self.mqtt = mqtt 57 | self.node_id = node_id 58 | self.discovery_prefix = discovery_prefix 59 | # Group wide extra conf, gets passed to sensors 60 | self.extra_conf = extra_conf 61 | # Read state_topic from config if provided 62 | if "state_topic" in extra_conf: 63 | self.state_topic = extra_conf["state_topic"] 64 | else: 65 | self.state_topic = discovery_prefix + b'/sensor/' + node_id + b'/state' 66 | extra_conf["state_topic"] = self.state_topic 67 | self.entities = [] 68 | 69 | def _update_extra_conf(self, extra_conf): 70 | if "value_template" not in extra_conf: 71 | raise Exception("Groupped sensors need value_template to be set.") 72 | extra_conf.update(self.extra_conf) 73 | 74 | def create_binary_sensor(self, name, object_id, extra_conf): 75 | self._update_extra_conf(extra_conf) 76 | bs = BinarySensor(self.mqtt, name, object_id, self.node_id, 77 | self.discovery_prefix, extra_conf) 78 | self.entities.append(bs) 79 | return bs 80 | 81 | def create_sensor(self, name, object_id, extra_conf): 82 | self._update_extra_conf(extra_conf) 83 | s = Sensor(self.mqtt, name, object_id, self.node_id, 84 | self.discovery_prefix, extra_conf) 85 | self.entities.append(s) 86 | return s 87 | 88 | def publish_state(self, state): 89 | self.mqtt.publish(self.state_topic, bytes(json.dumps(state), 'utf-8')) 90 | 91 | def remove_group(self): 92 | for e in self.entities: 93 | e.remove_entity() 94 | 95 | 96 | --------------------------------------------------------------------------------