Working example of HA integration

As someone who has developed 4 or 5 integrations now, ranging from fairly simple to large and complex and also helped a number of people get their developments off the ground, I thought I could use some of that experience to help others be more self starting and less afriad of having a go at developing an integration.

I see a lot of comments about how hard it is to get started on creating your own custom integration. I also, when looking at the scaffold integration, feel that there are bits missing that should be in there to give a good starter for 10 to someone new to developing for Home Assistant.

As such, I have tried to create some good example integrations, which can be installed and will work without any modification, that include the main foundational elements in commented code to use as a starter template in Integration 101 Template along with further examples of common other functionalities. I intend to create more examples as suggested/requested by people to build this into a go to place for how to do’s.

I will progress the documentation for each example but the code is well commented to try and explain what is happening as you follow it through.

I may also try to do some best practice guides but the Integration 101 Template is IMHO a best practice method and structure.

Please feel free to post questions or request topics you would like to see examples for.

Link to the repo…

17 Likes

This has now been updated with the readme completed and full comments in code.

Suggestions for examples of other functionality welcome to build up in this repo.

2 Likes

Hey Mark! Awesome work - I wish I could find this two weeks earlier when I started my fist custom integration - for now I’ve spend quite some time googling and debugging pretty basic stuff which is covered much better with your examples in comparison to current official docs. in addition, I’d love to see services exposure example (I’ve spend three days just to find out that for entity enabled services in order to make them appear in dev tools as service with entities as params listed for custom integrations you need to specify “entity”->“domain” and ->“integration” keys - without the latter entities are not listed for your custom service as parameters in HA Dev Tools - and this is not covered in any official docs as far as I was able to see). So in general here are few ideas for now (which I’m interesting as a noob) beside having clear understanding of best practices for custom integrations, does and don’ts, etc. Three topics I’m personally not clear about (I believe not only me!) and that might be next steps to extend your examples are: 1) Config Flow vs Integration Configuration via YAML - the question is not fully covered by official docs and I’m in doubts with my first integration - what are criteria on how my use-case needs to be best implemented 2) Question regarding own API and a lib hosted with Pypi - what are minimum criteria when you need to go that way.
I’ll try to go carefully though examples you’ve provided and put my questions/comments/suggestion. Might take quite some time for me but believe me, you’r doing dramendous work which is super-useful for a lot of noobs. Cheers!

1 Like

Great, glad this helps. I will look to do one on services but just to give you an answer on your immediate question.

Personally I would say always config flow. HA originally used YAML to setup integrations but has moved to config flow and now you must have config flow setup to be considered to be in the core. It is also much more user friendly and as you can see from my examples, not actually that difficult to do. I would say most of my integrations are little more than a copy/paste of the advanced config flow example.

Like a lot of integrations, some of my earlier ones started that way (as that was the only option) but then migrated over a few versions to config flow and after that, removed the YAML option all together.

For a custom integration, you can basically do what you like. There is no issue having your api as part of your custom component. If you want to have it in core, all APIs must be hosted on PyPI.

I tend to put apis on PyPI, just because once you are setup it is no great shakes to do, but entirely up to you.

Also, thanks for suggestion re Services. I’ll do that as my next one.

Feel free to ask questions, post links to your code for help etc.
Mark

I guess I’m the minority that would still prefer to have YAML as a base → so that makes me a bit sad…

In a perfect world, I’d like to keep GUI configuration for most of the time, but it would generate YAML for me to track with git. I don’t know if that’s possible with config flow → I guess it’s not easy, otherwise developers would just keep the YAML option.

Anyway, thanks for your examples, I intend to do my own integrations, so it will be helpful, for sure. I’ll probably see for myself how problematic is the config flow / yaml cooperation…

Yes, afraid it doesn’t work like that. The config flow does not create the yaml. They are seperate and an either or.

If you have a yaml config, you need the config flow to import this and create a config entry. Once imported, your Yaml is no longer used and you are now in the config flow world.

EDIT: with the caveat that you can keep importing your yaml to maintain a yaml config but not then change config in the UI and update yaml.

1 Like

Thank you, Mark! I’ve just posted my first integration with the state “it works for me” (GitHub - bpopovych/mqtt_relay_cover: MQTT Relay Cover Control for Home Assistant: A custom integration enabling smart control and tracking of relay-operated covers using MQTT. Transform your basic relays into intelligent devices for automation within Home Assistant.). I originally started from YAML config, so it is still there but I’m definitely considering to try Config Flow as per your suggestion.

And thanks for considering the example with service exposure.

With regards to the config - I like the idea by @DvdNwk - would be great to have simple human-readable config to track/keep your real home production HA state. I guess simply storing the DB might work here but you immediately get 1. possible version mismatch due to schema changes 2. non-human readable format - at least it take some time to understand the schema and you anyway need some external tool - some db viewer.

As an idea - maybe we can think of and offer some kind of feature improvement for custom integrations (or all of the integrations!) - some simple interface to provide the “store_config” service - and if implemented, it stores itself for future import/export if you wish. But that’s a raw concept - need to consider what’s already in HA (I guess I might simply not know that there is something like this) and config partitioning hell might be on your way right away with such kind of approach.

1 Like

The config generated by config flow is stored in a file and not in the db. In config/.storage/core.config_entries is where you see this. To backup, just save this file. However, it would be challenging to restore this on its own, as there are many other things to consider when doing this. You can however, copy and paste previously stored data and options keys (ie the config settings) if you are careful to ensure good json formatting.

Word of Warning: if you edit and break json formatting on this file, HA will not start!

EDIT: It is possible to have an integration be able to do this via service calls. Ie an integration with a service that lets you select an existing config entry, and write this out to a file. Then have another service to load a saved file into a loaded config entry. It would need to do checks that you are loading into the same integration and that the schema version is the same, but very doable.

Wow! Cool note, Mark @msp1974! That’s exactly what I was referring to as

Definitely worst to explore and maybe to try to develope some common module/lib to enable import/export for integrations based on these .storage config entity files might be something very interesting and valuable for the whole HA community!

Thanks so much for putting this together, I’m trying to build my first Integration that polls my API.
Are there any example that will actually poll a real API on a real IP address, and create multiple sensors?

My API is Json and I want to create many sensors and a couple of switches, any examples that might help me understand how to poll a real API you could point me towards would be greatly appreciated, I’m a C programmer, so getting my head around Python is challenging.

My API (http://192.168.1.1/api) returns:


{
	"device":	"Product1",
	"role":	"Master",
	"bms_num":	0,
	"can_protocol":	"",
	"type":	"type1",
	"code_version":	"3.01",
	"esp-idf_version":	"v5.1.2",
	"cores":	2,
	"chip_id":	123456789012345,
	"uptime":	"0d 16h 21m 4s",
	"charge_status":	"Bulk",
	"charge_enabled":	"ON",
	"charge_current":	50,
	"absorption_voltage":	56,
	"absorption_time":	30,
	"rebulk_offset":	2.5,
	"float_mode":	"OFF",
	"float_time":	6,
	"manual_charge":	"OFF",
	"battery_soh":	100,
	"slaves_total":	0,
	"force_chg_v":	44,
	"discharge_enabled":	"ON",
	"discharge_current":	100,
	"min_discharge_v":	48,
	"max_cycles":	6000,
	"cycles_offset":	0,
	"logs":	"OFF",
	"min_voltage_cell":	11,
	"max_voltage_cell":	13,
	"min_cell_voltage":	3.388,
	"max_cell_voltage":	3.398,
	"delta_cell_voltage":	0.01,
	"average_cell_voltage":	3.393,
	"cell_voltage_1":	3.394,
	"cell_voltage_2":	3.394,
	"cell_voltage_3":	3.393,
	"cell_voltage_4":	3.394
}

Thanks and Regards

I have created you an example, using your data output in:

msp_integration_requests_example

To explain it a little.

I have changed the api.py file to give an example of using the requests library to make a http api call. Requests is already included with HASS, so a good one to use.

As with the main 101 example, I have kept the DataUpdateCoordinator to call this api (you need to modify the call as just using the mock call to ensure the example works for all).

Then in sensors.py, I have shown how you can create multiple sensor types using a base class and other classes that inherit this. For ease, I have just created some constants that define which parameters should use which sensor class but you can do this in any way you see fit.

Other things to note.

  1. The device_info has changed to see all your sensor and binary sensor entities to be part of Device1.

  2. The config flow still has a username and password (same as 101 example) but you may need to adjust yours to suit.

Let me know how you get on. Screenshot below of what the example looks like when run:

Thanks for taking the time to make the example.

Having a few issues, it throws an error using the mock data, when adding the api, I have tries adding the path=“api” but no joy…

Logger: custom_components.msp_integration_requests_example.config_flow
Source: custom_components/msp_integration_requests_example/config_flow.py:92
integration: msp_integration_requests_example
First occurred: 22:44:50 (1 occurrences)
Last logged: 22:44:50

Unexpected exception
Traceback (most recent call last):
  File "/config/custom_components/msp_integration_requests_example/config_flow.py", line 92, in async_step_user
    info = await validate_input(self.hass, user_input)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/config/custom_components/msp_integration_requests_example/config_flow.py", line 56, in validate_input
    await hass.async_add_executor_job(api.get_mock_data)
  File "/usr/local/lib/python3.12/concurrent/futures/thread.py", line 58, in run
    result = self.fn(*self.args, **self.kwargs)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TypeError: API.get_mock_data() missing 1 required positional argument: 'path'

I assume i need to change this to get the real the data from web API:

        try:
            # TODO: Change this to use a real api call for data
            data = await self.hass.async_add_executor_job(self.api.get_mock_data, "api")
        except APIConnectionError as err:
            _LOGGER.error(err)
            raise UpdateFailed(err) from err
        except Exception as err:
            # This will show entities as unavailable by raising UpdateFailed exception
            raise UpdateFailed(f"Error communicating with API: {err}") from err

        # What is returned here is stored in self.data by the DataUpdateCoordinator
        return data

With something like this but I suspect I’m missing something?
from: Fetching data | Home Assistant Developer Docs

        try:
            # Note: asyncio.TimeoutError and aiohttp.ClientError are already
            # handled by the data update coordinator.
            async with async_timeout.timeout(10):
                # Grab active context variables to limit data required to be fetched from API
                # Note: using context is not required if there is no need or ability to limit
                # data retrieved from API.
                listening_idx = set(self.async_contexts())
                return await self.my_api.fetch_data(listening_idx)
        except ApiAuthError as err:
            # Raising ConfigEntryAuthFailed will cancel future updates
            # and start a config flow with SOURCE_REAUTH (async_step_reauth)
            raise ConfigEntryAuthFailed from err
        except ApiError as err:
            raise UpdateFailed(f"Error communicating with API: {err}")

Ah, sorry. This is what happens when you make tweaks quickly. In config_flow.py line 56, you need to change

await hass.async_add_executor_job(api.get_mock_data)

To

await hass.async_add_executor_job(api.get_mock_data, "api")

I’ll update the repo version later today.

Great thanks for confirming, I had changed that.

So now to get I got it to read my real API, had some unavailable errors when adding the integration:

EDIT:I fixed it, in the api.py , needs http like:

r = requests.get(f"http://{self.host}/{path}", timeout=10)

Now the fun task of adding switching with a html post to change the state!

Great. Yes i added a few deliberate typos to help you learn! :rofl::rofl:. Thanks will fix this in the example for others to use.

Yep it sure did help me learn!

I’m going pretty good getting all the sensors working, I’m having some issues with sensors that have a value of 0 do not get created, eg “slaves_total”

Also the time sensor does not get created
TIME_SENSORS = [“uptime”]

Can’t really figure out why they don’t get created, any ideas?

If you are using this structure

sensors.extend(
        [
            ExampleVoltageSensor(coordinator, parameter)
            for parameter in VOLTAGE_SENSORS
            if coordinator.get_api_data_value(parameter)
        ]
    )

Then it is testing for a True value. In Python, 0 is falsy, so you would need to change to

sensors.extend(
        [
            ExampleVoltageSensor(coordinator, parameter)
            for parameter in VOLTAGE_SENSORS
            if coordinator.get_api_data_value(parameter) is not None
        ]
    )

On the time sensor issue, you may need to show me your code.

I sorted the uptime sensor all good.

Now the most difficult part, getting the switches to work!

I have tried to create a generic “switch.py” but it doesn’t seem to create the switch let alone work, any idea where I have gone wrong?

in const.py , BATTERY_SWITCHES = [“charge_enabled”]
the switch I want to create.

"""Interfaces with the Integration 101 Template api sensors."""

import logging

from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity
from homeassistant.config_entries import ConfigEntry
# from homeassistant.const import (
#     PERCENTAGE,
#     UnitOfElectricCurrent,
#     UnitOfElectricPotential,
# )
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity

from .const import (
    DOMAIN,
    BATTERY_SWITCHES,
)
from .coordinator import ExampleCoordinator

_LOGGER = logging.getLogger(__name__)

async def async_setup_entry(
    hass: HomeAssistant,
    config_entry: ConfigEntry,
    async_add_entities: AddEntitiesCallback,
):
    """Set up the Switches."""
    # This gets the data update coordinator from hass.data as specified in your __init__.py
    coordinator: ExampleCoordinator = hass.data[DOMAIN][
        config_entry.entry_id
    ].coordinator

    # Enumerate all the sensors in your data value from your DataUpdateCoordinator and add an instance of your sensor class
    # to a list for each one.
    # This maybe different in your specific case, depending on how your data is structured
    switches = []

    # Our generic sensors
    switches.extend(
        [
            ExampleBaseSwitch(coordinator, parameter)
            for parameter in BATTERY_SWITCHES
            if coordinator.get_api_data_value(parameter)
        ]
    )

    # Now add the sensors.
    async_add_entities(switches)

class ExampleBaseSwitch(CoordinatorEntity, SwitchEntity):
    """Implementation of a switch."""

    def __init__(self, coordinator: ExampleCoordinator, parameter_name: str) -> None:
        """Initialise switch."""
        super().__init__(coordinator)
        self.parameter = parameter_name
        self.parameter_value = coordinator.get_api_data_value(self.parameter)

    @callback
    def _handle_coordinator_update(self) -> None:
        """Update sensor with latest data from coordinator."""
        # This method is called by your DataUpdateCoordinator when a successful update runs.
        self.parameter_value = self.coordinator.get_api_data_value(self.parameter)
        _LOGGER.debug("Parameter: %s, Value: %s", self.parameter, self.parameter_value)
        self.async_write_ha_state()

    @property
    def device_class(self) -> str:
        """Return device class."""
        # https://developers.home-assistant.io/docs/core/entity/switch/#available-device-classes
        return SwitchDeviceClass.SWITCH

    @property
    def device_info(self) -> DeviceInfo:
        """Return device information."""
        # Identifiers are what group entities into the same device.
        # If your device is created elsewhere, you can just specify the indentifiers parameter.
        # If your device connects via another device, add via_device parameter with the indentifiers of that device.
        device_name = self.coordinator.get_api_data_value("device")
        return DeviceInfo(
            name=device_name,
            manufacturer="ACME Manufacturer",
            model=self.coordinator.get_api_data_value("role"),
            sw_version=self.coordinator.get_api_data_value("code_version"),
            identifiers={(DOMAIN, f"{device_name}")},
        )

    @property
    def name(self) -> str:
        """Return the name of the sensor."""
        # Make nive name format by replacing parameter name _'s and title case
        return self.parameter.replace("_", " ").title()

    @property
    def native_value(self) -> int | float:
        """Return the state of the entity."""
        # Using native value and native unit of measurement, allows you to change units
        # in Lovelace and HA will automatically calculate the correct value.
        return self.parameter_value

    @property
    def unique_id(self) -> str:
        """Return unique id."""
        # All entities must have a unique id.  Think carefully what you want this to be as
        # changing it later will cause HA to create new entities.
        return (
            f"{DOMAIN}-{self.coordinator.get_api_data_value("device")}-{self.parameter}"
        )

    @property
    def is_on(self):
        """Get device state"""
        return self._state

    def turn_on(self, **_kwargs):
        """Turn on device"""
        self._dev.turn_on()
        self._state = True
        self._update_ha_state()

    def turn_off(self, **_kwargs):
        """Turn off device"""
        self._dev.turn_off()
        self._state = False
        self._update_ha_state()

Have you added it to the list of platforms to load in your init.py?

That was the missing link, so now the switch get created, hoary…

It doesn’t work as I expected it wouldn’t but at least it got it created…lol

So Imagine I have to create another api function to post the data back to the API?
I need to http post charge_enabled = “OFF” or “ON” bases on switch position.