Working example of HA integration

It can be hard to find integrations supporting serial from the code as the connection logic is typically handled by a separate library (separation is required for core integrations)

There seem to be more integrations using serial when searching for “serial” on https://www.home-assistant.io/ However most of those results are from integrations that have yaml setup where you specify the serial port. Since these are still using yaml they are probably older integrations following old best practices, not sure if I would recommend these as reference.

I am not sure how relevant it is that your integration will be using serial.

Independant of connection/protocol most API packages that I have worked with are like the api.py in the msp_integration_101_intermediate example.

  • There is an init() with sometimes a separate connect() or open() method if these are expensive operations, but often also combined in the init. This is probably where you would setup your eventloop.
  • Next to that there is a usually a way to get data like with get_mock_data() in the example which retrieves all data, or for individual attributes. All vs per attribute usually depends on the API you are working with.
  • And last there is a way to set/write things to the API like set_moc_data() in the example.

An instance of this API is created during integration setup and used by the entities later to get or set data. In the example a coordinator is used to poll the data instead of doing it in the update() method of the entity itself. Using a coordinator makes it easier to have one place to update your data and make it accessible to multiple entities. I would say using a coordinator is the standard nowadays.

As for a fully commented working example I think the examples provided in this thread is about the best you can get right now. I think the code generated by the scaffold also contains some useful comments but less than these examples.

If you have specific questions I would suggest to push your code to Github so people can look at the whole (even when it is not done yet). You could create a thread here or ask a question in #dev_core on Discord.

Thank you both. I will probably do a bit of both. @msp1974 I’ll send you my class via PM and I will publish it on github so I can post a new thread for assistance.

I suppose that it isn’t serial per se that I feel the issue is but using asyncio and specifically needing to run tasks. As I indicated above the docs seem to not want you doing any IO outside of the update function but that doesn’t work when you need a tasks that are reading and responding to the device. I guess you would have the same problem if it was say connecting to a TCP socket on a remote device and spawning tasks to read/write to the device. I guess I don’t understand where you would run your own tasks in this framework. Perhaps I’m being obtuse, hopefully MSP1974 can help me out a bit.

Thank you again.

Hi,

I been using the temples you helped me with and they have been working great.
I’m getting 2 errors in the logs that suggest I need to update "async_forward_entry_setup " as it is be deprecated.

Errors in the logs:

  • Detected that custom integration ‘example’ calls async_forward_entry_setup for integration, which is deprecated and will stop working in Home Assistant 2025.6, await async_forward_entry_setups instead at custom_components/example/init.py, line 73: hass.async_create_task(, please report it to the author of the ‘example’ custom integration

  • Detected code that calls async_forward_entry_setup for integration example with title: example Integration - device and entry_id: 01J5VW4BDSMCWYTXQMXKEYA840, during setup without awaiting async_forward_entry_setup, which can cause the setup lock to be released before the setup is done. This will stop working in Home Assistant 2025.1. Please report this issue

Any advice on how to update this, I tired to replace it with “await async_forward_entry_setups” but get the following error

  • Error setting up entry Integration
    Traceback (most recent call last):
    File “/usr/src/homeassistant/homeassistant/config_entries.py”, line 604, in async_setup
    result = await component.async_setup_entry(hass, self)

Regards

Hi James,

You need to change

for platform in PLATFORMS:
        hass.async_create_task(
            hass.config_entries.async_forward_entry_setup(config_entry, platform)
        )

In init.py
To

await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)

I’ll update the templates when i get time.

Hi Mark - your examples are great, fantastic work!

I’ve dabbled a bit with Config Flow and have a reasonable understanding of the basics, but one thing I’m looking into now is dynamic fields or callback-based fields, for example typing an address into one text field and running a periodic address lookup API call and populating a second text field.

I’ve done some research and can’t find anything relating to callbacks or similar, the best thing I can see is to have a two-step process where the code that runs the second step invokes the API call, processes the return and prepopulates the second step fields.

Do you have any thoughts or suggestions at all?

Cheers,
Andy

I dont think there is any way to have a callback, so the way you are planning (by having a 2 step process) is probably the way to go.

Hello Mark and others,

I’m wokring on a custom integration at the moment and it seems like I’m probably misunderstanding a core concept of the coordinator or missing one crucial line of code.

my init.py:

async def async_setup_entry(hass: HomeAssistant, entry: config_entries.ConfigEntry):
    """Set up IQ Stove from a config entry."""
    print("__init__ async setup entry")
    print(entry.data)
    stove = IQstove(entry.data["host"], 8080)
    coordinator = IQStoveCoordinator(hass, stove)
    await coordinator.async_config_entry_first_refresh()
    print("__init__ async setup entry after await coordinator refresh")
    # Store the coordinator in the hass data
    hass.data.setdefault(DOMAIN, {})
    hass.data[DOMAIN][entry.entry_id] = coordinator
    await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
    return True

coordinator.py:

class IQStoveCoordinator(DataUpdateCoordinator):
    """Class to manage the fetching of data from the IQ Stove."""

    def __init__(self, hass: HomeAssistant, stove: IQstove, update_interval: int = 30):
        print("Coordinator Init")
        """Initialize the coordinator."""
        self.stove = stove
        self.update_interval = timedelta(update_interval)

        super().__init__(
            hass,
            _LOGGER,
            name="IQ Stove",
            update_method=self.async_update_data,
            update_interval=timedelta(update_interval),
        )

    async def _async_setup(self):
        """Set up the coordinator

        This is the place to set up your coordinator,
        or to load data, that only needs to be loaded once.

        This method will be called automatically during
        coordinator.async_config_entry_first_refresh.
        """
        print("Coordinator Async Setup")
        if not self.stove.connected:
            await self.stove.connect()
        for cmd in self.stove.Commands.info:
            self.stove.getValue(cmd)
        await asyncio.sleep(0.5)
        print(self.stove.values)
        # print("After AsyncIO Create task")

    async def async_update_data(self):
        """Fetch data from the IQ Stove."""
        # await self.stove.sendPeriodicRequest()  # You can adjust this based on your needs
        if not self.stove.connected:
            await self.stove.connect()
        for cmd in self.stove.Commands.state:
            self.stove.getValue(cmd)
        await asyncio.sleep(0.5)
        print("Coordinator Async Update Data", self.stove.values)
        return self.stove.values

sensor.py:

async def async_setup_entry(
    hass: HomeAssistant,
    config_entry: ConfigEntries.ConfigEntry,
    async_add_entities: AddEntitiesCallback,
):
    """Setup sensors from a config entry created in the integrations UI."""
    print("Sensor Async Setup")
    coordinator: IQStoveCoordinator = hass.data[DOMAIN][config_entry.entry_id]
    sensors = [temperatureSensor(coordinator)]
    async_add_entities(sensors, update_before_add=True)


class temperatureSensor(CoordinatorEntity, SensorEntity):
    """Representation of a Sensor."""

    def __init__(self, coordinator: IQStoveCoordinator):
        """Pass coordinator to CoordinatorEntity."""
        super().__init__(coordinator)
        print("Temperaturesensor init")

    @callback
    def _handle_coordinator_update(self) -> None:
        """Handle updated data from the coordinator."""
        print("Sensor Handle Coordinator Update", self.coordinator.data)
        self._attr_native_value = self.coordinator.data["appT"]
        self.async_write_ha_state()

    @property
    def name(self) -> str:
        """Return the name of the sensor."""
        return f"{DOMAIN}-temperature"

    @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.
        # await self.coordinator.async_request_refresh()
        print("Sensor Native Value", self.coordinator.data["appT"])
        # return float(self.coordinator.getValue("appT"))
        return float(self.coordinator.data["appT"])

    @property
    def native_unit_of_measurement(self) -> str | None:
        """Return unit of temperature."""
        return UnitOfTemperature.CELSIUS

    @property
    def state_class(self) -> str | None:
        """Return state class."""
        # https://developers.home-assistant.io/docs/core/entity/sensor/#available-state-classes
        return SensorStateClass.MEASUREMENT

    @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}-temperature"

I thought, that the async_update_data function in the coordinator would be periodically called by homeassistant. And this would in turn call to the callback function _handle_coordinator_update in sensor.py.

From what I can see at the moment is that async_update_data is being called two times during startup. After that it never gets called again. Even worse, the callback function is not executed once.
I was only able to get the value into the sensor with native_value at startup. Which means that async_update_data ran successfully and populated coordinator.data.

Any help would be apreciated :slight_smile:

Its better to post your whole code and not just parts as it is easier to help you by seeing the whole thing.

I cant see anything obviously wrong in the snipets above, so def need to see the rest.

1 Like

There is not much more except for the imports and old commented code on the bottom. And then my API library for the communication in IQstove.py.

I have uploaded everything here: GitHub - Xsez/HomeAssistantIQStove

So the coordinator is supposed to behave the way i described?
Thanks a lot already for taking a look

Yes it does work how you describe. Let me take a proper look tomorrow and see if i can see what is wrong.

EDIT: to clarify, the 2 updates you see are you calling

coordinator.async_config_entry_first_refresh()

And update_before_add=True when adding entities. It should then call async_update_data every 30s after that and call your _handle_coordinator_update with a successful update.

It relies on having entities subscribed to the coordinator (which is a common mistake i see) but looks like you are doimg that correctly.

1 Like

Had a look through your code and the only thing i can see that maybe causing an issue is that you are importing a SCAN_INTERVAL constant in sensor.py.

This is a special constant that you can use if the sensor should poll when not using an UpdateCoordinator. I wonder if that somehow conflicts. It is not needed so try removing.

If that doesnt work, i’ll have to try running it and see what is going on.

1 Like

I have removed the SCAN_INTERVAL import in sensor.py. Unfortunately, no change.

OK, let me put in my dev environment and have a look.

You have a function async def async_update_data(self):

Isn’t that supposed to be async def _async_update_data(self):?

Note the leading underscore

FYI nowadays tracking the coordinator can be done in the runtime_data attribute of the configentry instead of hass.data. See Store runtime data inside the config entry | Home Assistant Developer Docs for more details

No that’s if you do not specifiy it with update_method when initialising.

Issue is that you are specifiying your timedelta incorrectly.

It should be:

update_interval=timedelta(seconds=update_interval),

and not

update_interval=timedelta(update_interval),
1 Like

Oh ffs. How embarrassing… Its now working great.

Thank you and sorry for taking up your time. :slight_smile:

1 Like

Hi,

I’m adding some additional sensor data to my JSON API, it looks like the below, note the child: battery_1, batery_2:

Wondering if I need to process child data differently in api.py ?

{
	"temperature_sensors":	0,
	"temperature_sensor_1":	30,
	"total_voltage":	16.36,
	"current":	0,
	"power":	0,
	"charging_power":	0,
	"discharging_power":	0,
	"charged_energy":	0,
	"discharged_energy":	0,
	"total_battery_capacity_setting":	0,
	"battery_0":	{
		"total_voltage":	16.345,
		"current":	0,
		"power":	0,
		"bms_capacity_remaining":	76,
		"battery_soh":	100,
		"min_voltage_cell":	1,
		"max_voltage_cell":	3,
		"min_cell_voltage":	4.009,
		"max_cell_voltage":	4.291,
		"delta_cell_voltage":	0.282,
		"average_cell_voltage":	5.448,
		"cell_voltage_1":	4.01,
		"cell_voltage_2":	4.016,
		"cell_voltage_3":	4.291,
		"cell_voltage_4":	4.028,
	},
	"battery_1":	{
		"total_voltage":	16.435,
		"current":	0,
		"power":	0,
		"bms_capacity_remaining":	76,
		"battery_soh":	100,
		"min_voltage_cell":	1,
		"max_voltage_cell":	3,
		"min_cell_voltage":	4.01,
		"max_cell_voltage":	4.291,
		"delta_cell_voltage":	0.281,
		"average_cell_voltage":	4.092,
		"cell_voltage_1":	4.01,
		"cell_voltage_2":	4.016,
		"cell_voltage_3":	4.291,
		"cell_voltage_4":	4.051,
	}
}

You’ll have to post a link to your code to remind me how it looks to answer that.

I think i need to iterate over the JSON of battery_* to get the values, and then add an Identifier eg “battery_0” for the sensor so they are unique in HA?

Something like:

for key in API_Data.battery_1:{ 
    retun(key,":", API_Data.battery_1[key]) 
}

I’m using the pretty generic api.py. Looks like:

    def post_data(self, path: str, parameter: str, value: Any) -> bool:
        """Post api data."""
        try:
            data = {parameter, value}
            r = requests.post(f"http://{self.host}/{path}", f"{parameter}={value}", timeout=10)
        except requests.exceptions.ConnectTimeout as err:
            raise APIConnectionError("Timeout connecting to api") from err
        else:
            return True

    def get_data(self, path: str) -> dict[str, Any]:
        """Get api data."""
        try:
            r = requests.get(f"http://{self.host}/{path}", timeout=10)
            return r.json()
        except requests.exceptions.ConnectTimeout as err:
            raise APIConnectionError("Timeout connecting to api") from err

coordinator.py:

        # Initialise your api here
        self.api = API(host=self.host, user=self.user, pwd=self.pwd)

    async def async_update_data(self):
        """Fetch data from API endpoint.

        This is the place to pre-process the data to lookup tables
        so entities can quickly look up their data.
        """
        try:
            # TODO: Change this to use a real api call for data
            data = await self.hass.async_add_executor_job(self.api.get_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

    def get_api_data_value(self, parameter_name: str) -> str | int | float:
        """Return parameter by name."""
        # Called by the sensors to get their updated data from self.data
        try:
            return self.data.get(parameter_name)
        except KeyError:
            return None

So, I would recommend altering your json to be more like…

{
  "temperature_sensors": 0,
  "temperature_sensor_1": 30,
  "total_voltage": 16.36,
  "current": 0,
  "power": 0,
  "charging_power": 0,
  "discharging_power": 0,
  "charged_energy": 0,
  "discharged_energy": 0,
  "total_battery_capacity_setting": 0,
  "batteries": [
    {
      "id": 0,
      "total_voltage": 16.345,
      "current": 0,
      "power": 0,
      "bms_capacity_remaining": 76,
      "battery_soh": 100,
      "min_voltage_cell": 1,
      "max_voltage_cell": 3,
      "min_cell_voltage": 4.009,
      "max_cell_voltage": 4.291,
      "delta_cell_voltage": 0.282,
      "average_cell_voltage": 5.448,
      "cell_voltage_1": 4.01,
      "cell_voltage_2": 4.016,
      "cell_voltage_3": 4.291,
      "cell_voltage_4": 4.028
    },
    {
      "id": 1,
      "total_voltage": 16.435,
      "current": 0,
      "power": 0,
      "bms_capacity_remaining": 76,
      "battery_soh": 100,
      "min_voltage_cell": 1,
      "max_voltage_cell": 3,
      "min_cell_voltage": 4.01,
      "max_cell_voltage": 4.291,
      "delta_cell_voltage": 0.281,
      "average_cell_voltage": 4.092,
      "cell_voltage_1": 4.01,
      "cell_voltage_2": 4.016,
      "cell_voltage_3": 4.291,
      "cell_voltage_4": 4.051
    }
  ]
}

As that is much easier to iterate through. And then in your sensor.py in async_setup_entry, you would do something like…

battery_sensors = [
    BatterySensorEntity(coordinator, battery_sensor)
    for battery_sensor in [battery for battery in coordinator.data["batteries"]]
]
add_entities(battery_sensors)

It will be a little different based on what is in your sensor.py (which is why you need to share whole code) but this is where you would iterate through you new data structure and create the sensors. You may also need another get_api_data_value for get_api_battery_data_value something like this.

def get_api_battery_data_value(self, battery_id: int, parameter_name: str) -> float:
    """Get battery data value."""
    for battery in self.data.get("batteries"):
        if battery.get("id") == battery_id:
            return battery.get(parameter_name)
1 Like