Howto write your own HomeAssistant integration
This article shows how to write your own home-assistant integration. When I wanted to write a mock for home-assistant it become really complicated. For a newbie to home-assistant the documentation was not very helpful. Or to be fair a simple and complete step-by-step guide would have been nice.
Requirements
- I used homeassistant-core commit hash 24ea5eb9b583e44aa170bd8806ddecce7ac09175 which was the latest in the moment of writing.
From scratch to configuration
Normally it should be easy to extend a popular platform like homeassistant with your own extension. For learning I like to build a waterheating device which is implemented as a heatpump-mock which can be integrated into homeassistant. I like to use the predifined Entity Water heater. You can also read the offical documentation for more information.
Basic understanding
Homeassistant distinguishes between integrations which are part of the homeassistant platform and integrations which are not.
In my understanding the difference becomes important at the moment you want to publish your integration.
If you have a real device you might think to put it to the core of homeassistant for me it was not intersting.
If you plan that your user can install it they can copy your integration to a folder custom_components
.
Lets go
Build integration as core-integration
For developing it is easier to develop your integration first as a core integration.
Prepare development environment
Lets follow the instructions from manual setup. In short:
- fork official homeassistant-core
- clone it to your local development environment
- prepare virtual python for homeassistant-environment -
script/setup
- start python virtual development environment -
source .venv/bin/activate
- start home assistant from terminal -
hass -c config
When everything goes well and not errors appeared you should be able to open and configure your local home-assistant. I only defined a user and a password and left everything else as default.
Steps for developing your integration
Run scaffold
Even if home assistant provides some information about howto creating your first integration this was of less use for me. The tool asks many questions, generate lots of code which is not explained and in the end did not worked out of the box. I hope you are smarter than me and the scaffold provide good results for you. For my article we start from scratch file after file.
Remove git-hooks
I like to use git and commit everytime I do some unknown steps because it allows me to rollback if changes do not have expected results. Unfortuantely the homeassistant core repository uses git hooks which are not of much help for the moment. So lets remove them
- Go to
.git/hooks
- The file
pre-commit
needs to be removed -rm pre-commit
Implement integration
- Its assumed your working directory is where you cloned the homeassistant repository to
- Stop the running homeassistant - press
CTRL+C
- create directory for your integration -
mkdir homeassistant/components/heatpump_mock
- the following steps will create several required files
manifest.json
This files is read by homeassistant to find your integration. See offical documentation for more details.
{
"domain": "heatpump_mock",
"name": "Heatpump mock",
"codeowners": ["@ehmkah"],
"config_flow": true,
"dependencies": [],
"documentation": "https://www.home-assistant.io/integrations/heatpump_mock",
"homekit": {},
"integration_type": "hub",
"iot_class": "local_polling",
"quality_scale": "bronze",
"version" : "0.0.1",
"requirements": []
}
Explanations:
domain
: Identify your integrationquality_scale
: Hommeassistant wants that integration fill minimum of quality. See https://developers.home-assistant.io/docs/core/integration-quality-scaleiot_class
: define how new values from your integration go to homeassistant we do local polling which makes homeassistant polling our integration again and againdocumentation
: Since we are building anoffical
component the documentation needs to be somewhere in homeassistant. Just use a uri which points somewhere knowone cares as long your integration should not be merged to offical homeassistant.version
: this is required if you plan to install it under custom_components on an other homeassistant-installation
const.py
This is not an officialy required files but it can be used to share some global constants for your integration.
"""Constants for the Heatpump-Mock integration."""
DOMAIN = "heatpump_mock"
Explanation
- nothing special
init.py
- This file is required because we are developing python. You find more information why this file is required here
"""The Heatpump-Mock integration."""
from __future__ import annotations
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from .coordinator import HeatpumpMockConfigEntry, MyApi
from .const import DOMAIN
from homeassistant.const import CONF_HOST, Platform
from homeassistant.helpers import device_registry
_PLATFORMS: list[Platform] = [Platform.WATER_HEATER]
async def async_setup_entry(hass: HomeAssistant,
entry: HeatpumpMockConfigEntry) -> bool:
"""Set up Heatpump-Mock from a config entry."""
entry.runtime_data = MyApi(hass,entry)
local_device_registry = device_registry.async_get(hass)
local_device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
identifiers={(DOMAIN, f"{entry.entry_id}_ws")},
manufacturer="HeatpumpMock",
sw_version="1.0",
)
await hass.config_entries.async_forward_entry_setups(entry, _PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: HeatpumpMockConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, _PLATFORMS)
Explanation:
_PLATFORMS: list[Platform] = [Platform.WATER_HEATER]
: tells homeassistant that we implement a waterheater other enties are available see https://developers.home-assistant.io/docs/core/entitydef async_async_setup_entry
: this method is called by homeassitant to load your integration device the classes you see here are definied below
config_flow.py
- in the manifest.json we said that we support configuration_flow so we need to provide that file, so homeassistant handle it correctly
"""Config flow for the Heatpump-Mock integration."""
from __future__ import annotations
import logging
from typing import Any
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.exceptions import ConfigEntryAuthFailed
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_HOST): str,
vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): str,
}
)
async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]:
"""Validate the user input allows us to connect.
Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user.
"""
if True==False:
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="cannot_authenticate",
)
# Return info that you want to store in the config entry.
return {"title": data[CONF_USERNAME]}
class ConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Heatpump-Mock."""
VERSION = 1
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
errors: dict[str, str] = {}
if user_input is not None:
try:
self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]})
info = await validate_input(self.hass, user_input)
except CannotConnect:
errors["base"] = "cannot_connect"
except InvalidAuth:
errors["base"] = "invalid_auth"
except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
return self.async_create_entry(title=info["title"], data=user_input)
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
)
class CannotConnect(HomeAssistantError):
"""Error to indicate we cannot connect."""
class InvalidAuth(HomeAssistantError):
"""Error to indicate there is invalid auth."""
Explanations
STEP_USER_DATA_SCHEMA = vol.Schema(
: defines what data the user needs to enter.
Homeassistant provides several values so it becomes simplier for the user to provide that information and some validation can be done be homeassistant.
coordinator.py
We use this file to define the configEntry.
A ConfigEntry is a configured entry for your integration.
In our case it only has a dummy MyApi
which can be extended if needed to do real calls.
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
type HeatpumpMockConfigEntry = ConfigEntry[MyApi]
class MyApi:
"""Heatpump-Mock API wrapper."""
config_entry: HeatpumpMockConfigEntry
def __init__(
self, hass: HomeAssistant,
entry: HeatpumpMockConfigEntry
) -> None:
self.config_entry = entry
return
quality_scale.yaml
This file works as ‘contract’ or checklist for yourself what needs to be done so your integration works as wanted by homeassistant. We pretend that we have everything done for level bronze - which is of course a lie :-P.
rules:
# Bronze
action-setup:
status: exempt
comment: no actions
appropriate-polling: done
brands: done
common-modules: done
config-flow-test-coverage: done
config-flow: done
dependency-transparency: done
docs-actions:
status: exempt
comment: no actions
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
entity-event-setup:
status: exempt
comment: no events
entity-unique-id: done
has-entity-name: done
runtime-data: done
test-before-configure: done
test-before-setup: done
unique-config-entry: done
# Silver
action-exceptions: todo
config-entry-unloading: todo
docs-configuration-parameters: todo
docs-installation-parameters: todo
entity-unavailable: todo
integration-owner: todo
log-when-unavailable: todo
parallel-updates: todo
reauthentication-flow: todo
test-coverage: todo
# Gold
devices: todo
diagnostics: todo
discovery-update-info: todo
discovery: todo
docs-data-update: todo
docs-examples: todo
docs-known-limitations: todo
docs-supported-devices: todo
docs-supported-functions: todo
docs-troubleshooting: todo
docs-use-cases: todo
dynamic-devices: todo
entity-category: todo
entity-device-class: todo
entity-disabled-by-default: todo
entity-translations: todo
exception-translations: todo
icon-translations: todo
reconfiguration-flow: todo
repair-issues: todo
stale-devices: todo
# Platinum
async-dependency: todo
inject-websession: todo
strict-typing: todo
strings.json
- This file contains the translations for your integration
- after implementing it something needs to be generated out of it
{
"config": {
"step": {
"user": {
"data": {
"host": "[%key:common::config_flow::data::host%]",
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]"
},
"data_description": {
"host": "[%key:common::config_flow::data::host%]",
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]"
}
}
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
}
}
water_heater.py
This is the file which does the implentation of the waterheater entity. You can implement more methods if this is useful for you here we just want to do the basics.
"""Simple waterheater mock"""
from __future__ import annotations
from typing import Any
from .coordinator import HeatpumpMockConfigEntry
from homeassistant.components.water_heater import (
WaterHeaterEntity,
WaterHeaterEntityFeature,
)
from homeassistant.const import UnitOfTemperature, ATTR_TEMPERATURE
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
async def async_setup_entry(
hass: HomeAssistant,
entry: HeatpumpMockConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Add Airzone Water Heater from a config_entry."""
coordinator = entry.runtime_data
async_add_entities([WaterHeaterMock()])
class WaterHeaterMock(WaterHeaterEntity):
"""Define an Airzone Water Heater."""
_attr_is_on = True
_attr_name = None
def __init__(self):
"Initialize the mock"
self._attr_temperature_unit = UnitOfTemperature.CELSIUS
self._attr_current_temperature=10
self._attr_target_temperature = 10
self._attr_unique_id='the mock'
self._attr_supported_features = (
WaterHeaterEntityFeature.TARGET_TEMPERATURE
| WaterHeaterEntityFeature.ON_OFF
| WaterHeaterEntityFeature.OPERATION_MODE
)
async def async_update(self):
"""Update the mock."""
self._attr_current_temperature = self._attr_current_temperature + 1
self.async_write_ha_state()
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the water heater off."""
self._attr_is_on = True
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the water heater off."""
self._attr_is_on = False
async def async_set_temperature(self, **kwargs: Any) -> None:
#"""Set new target temperature."""
#params: dict[str, Any] = {}
#if ATTR_TEMPERATURE in kwargs:
# params[API_ACS_SET_POINT] = kwargs[ATTR_TEMPERATURE]
self._attr_target_temperature = kwargs[ATTR_TEMPERATURE]
self.async_write_ha_state()
Build integration
Now all files which are required for the moment have been developed. We need to call several tools from homeassistant to see it in local homeassistant.
- create translation -
python -m script.translations develop --integration heatpump_mock
- when called successfully you see a
translations
directory inheatpump_mock
- when called successfully you see a
- make integration available in homeassistant -
python3 -m script.hassfest
- if you copied everything correctly and made no changes that ebverything is fine you will see something like
Integrations: 1373 Invalid integrations: 0
Run integration
- You implemented and builded your application time to run it
- start homeassistant -
hass -c config
- open your homeassistant in browser : http://localhost:8123
- goto http://localhost:8123/config/integrations/dashboard
- click on add integration
- search for
mock
- select found heatpump mock
- enter the requried parameters (it does not matter what you enter nothing happens to them)
- add name for device
- When you click on overview you should see the device
Congratulations you now have working minimal waterheater. I hope you can continue from here on your own.
Deploy integration
- Until now you build an integration in a way that you plan to merge it to homeassistant core
- if this is not the way you want to go because you want to distribute on your own or your pull-requests are declined than do the following
- you need access to your homeassistant installation - follow https://lazyadmin.nl/smart-home/enable-ssh-home-assistant/ for configuring it (Do not forget to enter the port number)
- start the installation of homeassistant installation
- copy heatpump_mock to
config/custom_components
- important: the name of the directory you copy and the domain name in manifest.json need to match!
- restart homeassistant
- add your integration as described in run integration
Summary
Let’s hope that this documentation worked for you.