esphome-docs/components/sensor/custom.rst

398 lines
15 KiB
ReStructuredText
Raw Normal View History

2018-06-04 13:46:40 +02:00
Custom Sensor Component
=======================
2018-11-14 22:12:27 +01:00
.. seo::
2019-02-16 23:25:23 +01:00
:description: Instructions for setting up Custom C++ sensors with ESPHome.
2018-11-19 22:28:21 +01:00
:image: language-cpp.png
2018-11-14 22:12:27 +01:00
:keywords: C++, Custom
2018-06-04 13:46:40 +02:00
.. warning::
2019-02-16 23:25:23 +01:00
While I do try to keep the ESPHome configuration options as stable as possible
and back-port them, the ESPHome API is less stable. If something in the APIs needs
2018-06-04 13:46:40 +02:00
to be changed in order for something else to work, I will do so.
2019-02-16 23:25:23 +01:00
So, you just set up ESPHome for your ESP32/ESP8266, but sadly ESPHome is missing a sensor integration
2018-06-04 13:46:40 +02:00
you'd really like to have 😕. It's pretty much impossible to support every single sensor, as there are simply too many.
2019-02-16 23:25:23 +01:00
That's why ESPHome has a really simple API for you to create your own **custom sensors** 🎉
2018-06-04 13:46:40 +02:00
In this guide, we will go through creating a custom sensor component for the
`BMP180 <https://www.adafruit.com/product/1603>`__ pressure sensor (we will only do the pressure part,
temperature is more or less the same). During this guide, you will learn how to 1. define a custom sensor
2019-02-16 23:25:23 +01:00
ESPHome can use 2. go over how to register the sensor so that it will be shown inside Home Assistant and
2020-05-10 21:27:59 +02:00
3. leverage an existing Arduino library for the BMP180 with ESPHome.
2018-06-04 13:46:40 +02:00
.. note::
Since the creation of this guide, the BMP180 has been officially supported by the :doc:`BMP085 component
<bmp085>`. The code still applies though.
2018-11-16 18:39:26 +01:00
This guide will require at least a bit of knowledge of C++, so be prepared for that. If you've already written
code for an Arduino, you have already written C++ code :) (Arduino uses a slightly customized version of C++).
If you have any problems, I'm here to help: https://discord.gg/KhAMKrd
2018-06-04 13:46:40 +02:00
Step 1: Custom Sensor Definition
--------------------------------
2018-11-26 16:50:50 +01:00
To create your own custom sensor, you just have to create your own C++ class. If you've never heard of that
before, don't worry, at the end of this guide you can just copy the example source code and modify it to your needs
- learning the intricacies of C++ classes won't be required.
2018-06-04 13:46:40 +02:00
2018-11-26 16:50:50 +01:00
Before you can create your own custom sensors, let's first take a look at the basics: How sensors (and components)
2019-02-16 23:25:23 +01:00
are structured in the ESPHome ecosystem.
2018-06-04 13:46:40 +02:00
2019-02-16 23:25:23 +01:00
In ESPHome, a **sensor** is some hardware device (like a BMP180) that periodically
2018-11-26 16:50:50 +01:00
sends out numbers, for example a temperature sensor that periodically publishes its temperature **state**.
2018-06-04 13:46:40 +02:00
2019-02-16 23:25:23 +01:00
Another important abstraction in ESPHome is the concept of a **component**. In ESPHome,
2019-05-12 22:44:59 +02:00
a **component** is an object with a *lifecycle* managed by the :apiclass:`Application` class.
2018-11-26 16:50:50 +01:00
What does this mean? Well if you've coded in Arduino before you might know the two special methods
``setup()`` and ``loop()``. ``setup()`` is called one time when the node boots up and ``loop()`` is called
very often and this is where you can do things like read out sensors etc.
2018-06-04 13:46:40 +02:00
2018-11-26 16:50:50 +01:00
Components have something similar to that: They also have ``setup()`` and ``loop()`` methods which will be
2020-05-10 21:27:59 +02:00
called by the application kind of like the Arduino functions.
2018-06-04 13:46:40 +02:00
2018-11-26 16:50:50 +01:00
So, let's now take a look at some code: This is an example of a custom component class (called ``MyCustomSensor`` here):
2018-06-04 13:46:40 +02:00
2018-11-19 22:28:21 +01:00
.. code-block:: cpp
2018-06-04 13:46:40 +02:00
2019-02-16 23:25:23 +01:00
#include "esphome.h"
2018-11-26 16:50:50 +01:00
2019-05-12 22:44:59 +02:00
class MyCustomSensor : public Component, public Sensor {
2018-06-04 13:46:40 +02:00
public:
void setup() override {
// This will be called by App.setup()
}
void loop() override {
// This will be called by App.loop()
}
};
2019-05-12 22:44:59 +02:00
In the first two lines, we're importing ESPHome so you can use the APIs via the ``#include``
statement.
2018-06-04 13:46:40 +02:00
2018-11-16 18:39:26 +01:00
Let's now also take a closer look at this line, which you might not be too used to when writing Arduino code:
2018-06-04 13:46:40 +02:00
2018-11-19 22:28:21 +01:00
.. code-block:: cpp
2018-06-04 13:46:40 +02:00
2019-05-12 22:44:59 +02:00
class MyCustomSensor : public Component, public Sensor {
2018-06-04 13:46:40 +02:00
2018-11-26 16:50:50 +01:00
What this line is essentially saying is that we're defining our own class that's called ``MyCustomSensor``
2019-05-12 22:44:59 +02:00
which is also a subclass of :apiclass:`Component` and :apiclass:`Sensor <sensor::Sensor>`.
As described before, these two "parent" classes have special semantics that we will make use of.
2018-11-16 18:39:26 +01:00
2018-11-26 16:50:50 +01:00
We *could* go implement our own sensor code now by replacing the contents of ``setup()`` and ``loop()``.
In ``setup()`` we would initialize the sensor and in ``loop()`` we would read out the sensor and publish
the latest values.
2018-06-04 13:46:40 +02:00
2018-11-26 16:50:50 +01:00
However, there's a small problem with that approach: ``loop()`` gets called very often (about 60 times per second).
2019-05-12 22:44:59 +02:00
If we would publish a new state each time that method is called we would quickly make the node unresponsive.
2018-11-26 16:50:50 +01:00
2020-05-27 00:52:23 +02:00
So lets fix this, we will use an alternative class to :apiclass:`Component`: :apiclass:`PollingComponent`.
2018-11-27 17:05:23 +01:00
This class is for situations where you have something that should get called repeatedly with some **update interval**.
2019-05-12 22:44:59 +02:00
In the code above, we can simply replace :apiclass:`Component` by :apiclass:`PollingComponent` and
2018-11-27 17:05:23 +01:00
``loop()`` by a special method ``update()`` which will be called with an interval we can specify.
2018-06-04 13:46:40 +02:00
2018-11-19 22:28:21 +01:00
.. code-block:: cpp
2018-06-04 13:46:40 +02:00
2019-05-12 22:44:59 +02:00
class MyCustomSensor : public PollingComponent, public Sensor {
2018-06-04 13:46:40 +02:00
public:
2018-11-16 18:39:26 +01:00
// constructor
MyCustomSensor() : PollingComponent(15000) {}
2018-06-04 13:46:40 +02:00
void setup() override {
// This will be called by App.setup()
}
void update() override {
2018-11-16 18:39:26 +01:00
// This will be called every "update_interval" milliseconds.
2018-06-04 13:46:40 +02:00
}
};
2019-05-12 22:44:59 +02:00
Our code has slightly changed, as explained above we're now inheriting from :apiclass:`PollingComponent` instead of
just :apiclass:`Component`. Additionally, we now have a new line: the constructor. You also don't really need to
2018-11-26 16:50:50 +01:00
know much about constructors here, so to simplify let's just say this is where we "initialize" the custom sensor.
2019-05-12 22:44:59 +02:00
In this constructor we're telling the compiler that we want :apiclass:`PollingComponent` to be instantiated with an
2019-02-16 23:25:23 +01:00
*update interval* of 15s, or 15000 milliseconds (ESPHome uses milliseconds internally).
2018-11-16 18:39:26 +01:00
Let's also now make our sensor actually publish values in the ``update()`` method:
2018-06-04 13:46:40 +02:00
2018-11-19 22:28:21 +01:00
.. code-block:: cpp
2018-06-04 13:46:40 +02:00
2018-11-16 18:39:26 +01:00
// class MyCustomSensor ...
2018-06-04 13:46:40 +02:00
// ... previous code
void update() override {
2018-11-16 18:39:26 +01:00
publish_state(42.0);
2018-06-04 13:46:40 +02:00
}
};
2018-11-16 18:39:26 +01:00
Every time ``update`` is called we will now **publish** a new value to the frontend.
2019-02-16 23:25:23 +01:00
The rest of ESPHome will then take care of processing this value and ultimately publishing it
2018-11-16 18:39:26 +01:00
to the outside world (for example using MQTT).
2018-06-04 13:46:40 +02:00
Step 2: Registering the custom sensor
-------------------------------------
Now we have our Custom Sensor set up, but unfortunately it doesn't do much right now.
2018-11-26 16:50:50 +01:00
Actually ... it does nothing because it's never included nor instantiated.
First, create a new file called ``my_custom_sensor.h`` in your configuration directory and copy the source code
from above into that file.
2019-02-16 23:25:23 +01:00
Then in the YAML config, *include* that file in the top-level ``esphome`` section like this:
2018-11-26 16:50:50 +01:00
.. code-block:: yaml
2019-02-16 23:25:23 +01:00
esphome:
2018-11-26 16:50:50 +01:00
# ... [Other options]
includes:
- my_custom_sensor.h
Next, create a new ``custom`` sensor platform entry like this:
2018-06-04 13:46:40 +02:00
2018-11-19 22:28:21 +01:00
.. code-block:: yaml
2018-06-04 13:46:40 +02:00
2018-11-16 18:39:26 +01:00
# Example configuration entry
sensor:
- platform: custom
lambda: |-
auto my_sensor = new MyCustomSensor();
App.register_component(my_sensor);
return {my_sensor};
2018-06-04 13:46:40 +02:00
2018-11-16 18:39:26 +01:00
sensors:
name: "My Custom Sensor"
2018-06-04 13:46:40 +02:00
2018-11-16 18:39:26 +01:00
Let's break this down:
2018-06-04 13:46:40 +02:00
2018-11-16 18:39:26 +01:00
- First, we specify a :ref:`lambda <config-lambda>` that will be used to **instantiate** our sensor class. This will
2019-02-16 23:25:23 +01:00
be called on boot to register our sensor in ESPHome.
2018-11-16 18:39:26 +01:00
- In this lambda, we're first creating a new instance of our custom class (``new MyCustomSensor()``) and then
assigning it to a variable called ``my_sensor``. Note: This uses a feature in the C++ standard, ``auto``, to make our
lives easier. We could also have written ``MyCustomSensor *my_sensor = new MyCustomSensor()``
2019-02-16 23:25:23 +01:00
- Next, as our custom class inherits from Component, we need to **register** it - otherwise ESPHome will not know
2018-11-16 18:39:26 +01:00
about it and won't call our ``setup()`` and ``update`` methods!
- Finally, we ``return`` the custom sensor - don't worry about the curly braces ``{}``, we'll cover that later.
2019-02-16 23:25:23 +01:00
- After that, we just let ESPHome know about our newly created sensor too using the ``sensors:`` block. Additionally,
2018-11-16 18:39:26 +01:00
here we're also assigning the sensor a name.
Now all that's left to do is upload the code and let it run :)
2018-06-04 13:46:40 +02:00
If you have Home Assistant MQTT discovery setup, it will even automatically show up in the frontend 🎉
.. figure:: images/custom-ui.png
2018-11-26 16:50:50 +01:00
:align: center
2018-06-04 13:46:40 +02:00
:width: 60%
Step 3: BMP180 support
----------------------
2018-11-16 18:39:26 +01:00
Let's finally make this custom sensor useful by adding the BMP180 aspect into it! Sure, printing ``42`` is a nice number
but it won't help with home automation :D
2020-05-10 21:27:59 +02:00
A great feature of ESPHome is that you don't need to code everything yourself. You can use any existing Arduino
2018-11-16 18:39:26 +01:00
library to do the work for you! Now for this example we'll
2018-06-04 13:46:40 +02:00
use the `Adafruit BMP085 Library <https://platformio.org/lib/show/525/Adafruit%20BMP085%20Library>`__
2018-11-16 18:39:26 +01:00
library to implement support for the BMP085 sensor. But you can find other libraries too on the
2020-05-10 21:27:59 +02:00
`PlatformIO library index <https://platformio.org/lib>`__
2018-06-04 13:46:40 +02:00
2018-11-26 16:50:50 +01:00
First we'll need to add the library to our project dependencies. To do so, put ``Adafruit BMP085 Library``
in your global ``libraries``:
2018-06-04 13:46:40 +02:00
2018-11-26 16:50:50 +01:00
.. code-block:: yaml
2018-06-04 13:46:40 +02:00
2019-02-16 23:25:23 +01:00
esphome:
2018-11-26 16:50:50 +01:00
includes:
- my_custom_sensor.h
libraries:
- "Adafruit BMP085 Library"
2018-06-04 13:46:40 +02:00
2018-11-26 16:50:50 +01:00
Next, include the library at the top of your custom sensor file you created previously:
2018-06-04 13:46:40 +02:00
2018-11-19 22:28:21 +01:00
.. code-block:: cpp
2018-06-04 13:46:40 +02:00
2019-02-16 23:25:23 +01:00
#include "esphome.h"
2018-11-26 16:50:50 +01:00
#include "Adafruit_BMP085.h"
2018-06-04 13:46:40 +02:00
2018-09-26 22:25:36 +02:00
// ...
2018-11-26 16:50:50 +01:00
Then update the sensor for BMP180 support:
2018-06-04 13:46:40 +02:00
2018-11-19 22:28:21 +01:00
.. code-block:: cpp
2018-06-04 13:46:40 +02:00
2018-09-26 22:25:36 +02:00
// ...
2019-05-12 22:44:59 +02:00
class MyCustomSensor : public PollingComponent, public Sensor {
2018-06-04 13:46:40 +02:00
public:
Adafruit_BMP085 bmp;
2018-11-16 18:39:26 +01:00
MyCustomSensor() : PollingComponent(15000) { }
2018-06-04 13:46:40 +02:00
void setup() override {
bmp.begin();
}
void update() override {
2018-11-16 18:39:26 +01:00
int pressure = bmp.readPressure(); // library returns value in in Pa, which equals 1/100 hPa
publish_state(pressure / 100.0); // convert to hPa
2018-06-04 13:46:40 +02:00
}
};
2018-11-16 18:39:26 +01:00
// ...
2018-09-26 22:25:36 +02:00
2018-11-16 18:39:26 +01:00
There's not too much going on there. First, we define the variable ``bmp`` of type ``Adafruit_BMP085``
2020-05-10 21:27:59 +02:00
inside our class as a class member. This is the object the Adafruit library exposes and through which
2018-11-16 18:39:26 +01:00
we will communicate with the sensor.
2018-09-26 22:25:36 +02:00
2018-11-16 18:39:26 +01:00
In our custom ``setup()`` function we're *initializing* the library (using ``.begin()``) and in
``update()`` we're reading the pressure and publishing it using ``publish_state``.
2018-09-26 22:25:36 +02:00
2019-02-16 23:25:23 +01:00
For ESPHome we can use the previous YAML. So now if you upload the firmware, you'll see the sensor
2018-11-16 18:39:26 +01:00
reporting actual pressure values! Hooray 🎉!
2018-09-26 22:25:36 +02:00
2018-11-16 18:39:26 +01:00
Step 4: Additional Overrides
----------------------------
There's a slight problem with our code: It does print the values fine, **but** if you look in Home Assistant
you'll see a) the value has no **unit** attached to it and b) the value will be rounded to the next integer.
2019-02-16 23:25:23 +01:00
This is because ESPHome doesn't know these infos, it's only passed a floating point value after all.
2018-11-16 18:39:26 +01:00
We *could* fix that in our custom sensor class (by overriding the ``unit_of_measurement`` and ``accuracy_decimals``
2019-02-16 23:25:23 +01:00
methods), but here we have the full power of ESPHome, so let's use that:
2018-11-16 18:39:26 +01:00
2018-11-19 22:28:21 +01:00
.. code-block:: yaml
2018-11-16 18:39:26 +01:00
# Example configuration entry
sensor:
- platform: custom
lambda: |-
auto my_sensor = new MyCustomSensor();
App.register_component(my_sensor);
return {my_sensor};
sensors:
name: "My Custom Sensor"
unit_of_measurement: hPa
accuracy_decimals: 2
2018-06-04 13:46:40 +02:00
2018-09-26 22:25:36 +02:00
Bonus: Sensors With Multiple Output Values
------------------------------------------
2018-11-16 18:39:26 +01:00
The ``Sensor`` class doesn't fit every use-case. Sometimes, (as with the BMP180),
2018-09-26 22:25:36 +02:00
a sensor can expose multiple values (temperature *and* pressure, for example).
2019-02-16 23:25:23 +01:00
Doing so in ESPHome is a bit more difficult. Basically, we will have to change our sensor
2018-11-16 18:39:26 +01:00
model to have a **component** that reads out the values and then multiple **sensors** that represent
the individual sensor measurements.
2018-09-26 22:25:36 +02:00
Let's look at what that could look like in code:
2018-11-19 22:28:21 +01:00
.. code-block:: cpp
2018-09-26 22:25:36 +02:00
2018-11-16 18:39:26 +01:00
class MyCustomSensor : public PollingComponent {
2018-09-26 22:25:36 +02:00
public:
Adafruit_BMP085 bmp;
2019-05-12 22:44:59 +02:00
Sensor *temperature_sensor = new Sensor();
Sensor *pressure_sensor = new Sensor();
2018-09-26 22:25:36 +02:00
2018-11-16 18:39:26 +01:00
MyCustomSensor() : PollingComponent(15000) { }
2018-09-26 22:25:36 +02:00
void setup() override {
bmp.begin();
}
void update() override {
// This is the actual sensor reading logic.
float temperature = bmp.readTemperature();
temperature_sensor->publish_state(temperature);
2018-11-16 18:39:26 +01:00
int pressure = bmp.readPressure();
pressure_sensor->publish_state(pressure / 100.0);
2018-09-26 22:25:36 +02:00
}
};
2018-11-16 18:39:26 +01:00
The code here has changed a bit:
2018-11-19 22:28:21 +01:00
2018-11-16 18:39:26 +01:00
- Because the values are no longer published by our custom class, ``MyCustomSensor`` no longer inherits
from ``Sensor``.
- The class has two new members: ``temperature_sensor`` and ``pressure_sensor``. These will be used to
publish the values.
- In our ``update()`` method we're now reading out the temperature *and* pressure. These values are then
published with the temperature and pressure sensor instances we declared before.
2018-09-26 22:25:36 +02:00
2018-11-16 18:39:26 +01:00
Our YAML configuration needs an update too:
2018-09-26 22:25:36 +02:00
2018-11-19 22:28:21 +01:00
.. code-block:: yaml
2018-09-26 22:25:36 +02:00
2018-11-16 18:39:26 +01:00
# Example configuration entry
sensor:
- platform: custom
lambda: |-
auto my_sensor = new MyCustomSensor();
App.register_component(my_sensor);
return {my_sensor->temperature_sensor, my_sensor->pressure_sensor};
2018-09-26 22:25:36 +02:00
2018-11-16 18:39:26 +01:00
sensors:
- name: "My Custom Temperature Sensor"
unit_of_measurement: °C
accuracy_decimals: 1
- name: "My Custom Pressure Sensor"
unit_of_measurement: hPa
accuracy_decimals: 2
2018-09-26 22:25:36 +02:00
2019-02-16 23:25:23 +01:00
In ``lambda`` the return statement has changed: Because we have *two* sensors now we must tell ESPHome
2018-11-16 18:39:26 +01:00
about both of them. We do this by returning them as an array of values in the curly braces.
2018-09-26 22:25:36 +02:00
2018-11-16 18:39:26 +01:00
``sensors:`` has also changed a bit: Now that we have multiple sensors, each of them needs an entry here.
2018-09-26 22:25:36 +02:00
2018-11-16 18:39:26 +01:00
Note that the number of arguments you put in the curly braces *must* match the number of sensors you define in the YAML
``sensors:`` block - *and* they must be in the same order.
2018-09-26 22:25:36 +02:00
2018-11-19 22:28:21 +01:00
Configuration variables:
- **lambda** (**Required**, :ref:`lambda <config-lambda>`): The lambda to run for instantiating the
sensor(s).
- **sensors** (**Required**, list): A list of sensors to initialize. The length here
must equal the number of items in the ``return`` statement of the ``lambda``.
2019-02-17 12:28:17 +01:00
- All options from :ref:`Sensor <config-sensor>`.
2018-11-19 22:28:21 +01:00
2019-05-12 22:44:59 +02:00
Logging in Custom Components
----------------------------
It is possible to log inside of custom components too. You can use the provided ``ESP_LOGx``
functions for this.
.. code-block:: cpp
ESP_LOGD("custom", "This is a custom debug message");
// Levels:
// - ERROR: ESP_LOGE
// - WARNING: ESP_LOGW
// - INFO: ESP_LOGI
// - DEBUG: ESP_LOGD
// - VERBOSE: ESP_LOGV
// - VERY_VERBOSE: ESP_LOGVV
ESP_LOGD("custom", "The value of sensor is: %f", this->state);
See :ref:`display-printf` for learning about how to use formatting in log strings.
.. note::
On ESP8266s you need to disable storing strings in flash to use logging in custom code.
.. code-block:: yaml
logger:
level: DEBUG
esp8266_store_log_strings_in_flash: False
2018-06-04 13:46:40 +02:00
See Also
--------
- :ghedit:`Edit`