Logo TheStaticTurtle


Creating a custom component for home assistant

So after building a reliable enough software for my open433 board, I wanted to make add my project as a platform for home assistant.



So after building a reliable enough software for my open433 board, I wanted to make add my project as a platform for home assistant. However, I found official documentation by homeassistant.io very shitty when you don't know the inner workings of the platform so here's my take on it:

To create a home assistant component you need to create a folder named custom_components folder in the home assistant configuration directory (same one as the configuration.yaml) then in this older you need to create a folder with the name of your integration.

In this folder create a file name manifest.json this is where we will be declaring information about you integration:

1
2
3
4
5
6
7
{
  "domain": "open433",
  "name": "Open433 board",
  "documentation": "https://github.com/TheStaticTurtle/Open433",
  "requirements": ["pyserial==3.4"],
  "codeowners": ["@TheStaticTurtle"]
}

In here you specify the domain of your integration then the description the documentation of the project the requirements needed by you program (in my case pyserial) and then the GitHub usernames of who made the code

Then in the same folder create a file named init.py in this file you will need to declare the configuration needed by your integration. So first import some constants / libraries:

1
2
3
4
5
6
import logging
from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP
import homeassistant.helpers.config_validation as cv
import voluptuous as vol
import threading
from . import rcswitch

in my case I have the library that I made in the same folder (named rcswitch.py). Then you need to declare some stuff like the domain (the name of your integration) , the name of the parameters used by the configuration and finally the configuration schema:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
_LOGGER = logging.getLogger(__name__)

DOMAIN = "open433"

CONF_COMPORT = "port"
CONF_COMSPEED = "speed"

REQ_LOCK = threading.Lock()
CONFIG_SCHEMA = vol.Schema(
	{
		DOMAIN: vol.Schema({
			vol.Required(CONF_COMPORT): cv.string,
			vol.Optional(CONF_COMSPEED, default=9600): cv.positive_int,
		})
	},
	extra=vol.ALLOW_EXTRA,
)

So for me the configuration could look like this:

1
2
open433:
  port: COM3

That's it for declaring the constants now you need to declare the actual setup function my one looks like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
def setup(hass, config):
	conf = config[DOMAIN]
	comport = conf.get(CONF_COMPORT)
	comspeed = conf.get(CONF_COMSPEED)

	rf = rcswitch.RCSwitch(comport, speed=comspeed)
	rf.libWaitForAck(True, timeout=1)

	def cleanup(event):
		rf.cleanup()

	def prepare(event):
		rf.prepare()
		rf.startReceivingThread()
		hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, cleanup)

	hass.bus.listen_once(EVENT_HOMEASSISTANT_START, prepare)
	hass.data[DOMAIN] = rf

	return True

To break it down first we get the configuration from home assistant config for the current domain (line 2) then we retrieve the config (line3-4). Then specific to me I initialize my library (line6-7) then we define function for home assistant to execute while it start stops (line9-17) and then I store the instance of my library into home assistant data for the specific domain.

And that all for the __init__.py

After all this you need to add the component for the platform as my project controls ac rf switches I wanted to make a switch component, to do that simply create a switch.py in the same folder

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import logging

import voluptuous as vol

from homeassistant.components.switch import SwitchEntity, PLATFORM_SCHEMA
from homeassistant.const import CONF_NAME, CONF_SWITCHES
import homeassistant.helpers.config_validation as cv
from . import DOMAIN, REQ_LOCK, rcswitch

_LOGGER = logging.getLogger(__name__)

CONF_CODE_OFF = "code_off"
CONF_CODE_ON = "code_on"
CONF_PROTOCOL = "protocol"
CONF_LENGTH = "length"
CONF_SIGNAL_REPETITIONS = "signal_repetitions"
CONF_ENABLE_RECEIVE = "enable_receive"

SWITCH_SCHEMA = vol.Schema(
	{
		vol.Required(CONF_CODE_OFF): vol.All(cv.ensure_list_csv, [cv.positive_int]),
		vol.Required(CONF_CODE_ON): vol.All(cv.ensure_list_csv, [cv.positive_int]),
		vol.Optional(CONF_LENGTH, default=32): cv.positive_int,
		vol.Optional(CONF_SIGNAL_REPETITIONS, default=15): cv.positive_int,
		vol.Optional(CONF_PROTOCOL, default=2): cv.positive_int,
		vol.Optional(CONF_ENABLE_RECEIVE, default=False): cv.boolean,
	}
)

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
	{
		vol.Required(CONF_SWITCHES): vol.Schema({cv.string: SWITCH_SCHEMA}),
	}
)

The start is very similar to the init file you import everything and define what the configuration will look like. This what my configuration looks like (the two first line are home assistant specific)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
switch:
  - platform: open433
    switches:
      switchA:
        code_on: 2389577216
        code_off: 2171473408
        protocol: 2
        length: 32
        signal_repetitions: 5
        enable_receive: true

Then you need to set up the platform

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
def setup_platform(hass, config, add_entities, discovery_info=None):
	rf = hass.data[DOMAIN]

	switches = config.get(CONF_SWITCHES)
	devices = []
	for dev_name, properties in switches.items():
		devices.append(
			Open433Switch(
				properties.get(CONF_NAME, dev_name),
				rf,
				properties.get(CONF_PROTOCOL),
				properties.get(CONF_LENGTH),
				properties.get(CONF_SIGNAL_REPETITIONS),
				properties.get(CONF_CODE_ON),
				properties.get(CONF_CODE_OFF),
				properties.get(CONF_ENABLE_RECEIVE),
			)
		)

	add_entities(devices)

First I retrieve the rf instance that was stored earlier in the init, get the array of switches in the configuration iterate over them and then declare each device (the Open433Switch class) and tell home assistant about my entities. So about he Open433Switch:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
class Open433Switch(SwitchEntity):
	def __init__(self, name, rf, protocol, length, signal_repetitions, code_on, code_off,enable_rx):
		self._name = name
		self._state = False
		self._rf = rf
		self._protocol = protocol
		self._length = length
		self._code_on = code_on
		self._code_off = code_off
		self._signal_repetitions = signal_repetitions
		if enable_rx:
			self._rf.addIncomingPacketListener(self._incoming)

	def _incoming(self, packet):
		if isinstance(packet, rcswitch.packets.ReceivedSignal):
			if packet.length == self._length and packet.protocol == self._protocol:
				if packet.decimal in self._code_on:
					self._state = True
					self.schedule_update_ha_state()
				if packet.decimal in self._code_off:
					self._state = False
					self.schedule_update_ha_state()

	@property
	def should_poll(self):
		return False

	@property
	def name(self):
		return self._name

	@property
	def is_on(self):
		return self._state

	def _send_code(self, code_list, protocol, length):
		with REQ_LOCK:
			self._rf.setRepeatTransmit(self._signal_repetitions)
			for code in code_list:
				packet = rcswitch.packets.SendDecimal(value=code, length=length, protocol=protocol, delay=700)
				self._rf.send(packet)
		return True

	def turn_on(self, **kwargs):
		if self._send_code(self._code_on, self._protocol, self._length):
			self._state = True
			self.schedule_update_ha_state()

	def turn_off(self, **kwargs):
		if self._send_code(self._code_off, self._protocol, self._length):
			self._state = False
			self.schedule_update_ha_state()

Basically in the class the thing you need to worry about is the Open433Switch() that is expanding the SwitchEntity class, the should_poll propriety, the name propriety, the is_on propriety, the turn_on function and the turn_off function.

These function/ proprieties executes / get polled at different time. Example when you change the status in the dashboard the turn_on / turn_off function get executed. When the code executes self.schedule_update_ha_state() the proprieties get polled

And that's the basics

Integration file structure | Home Assistant Developer Docs

Each integration is stored inside a directory named after the integration domain. The domain is a short name consisting of characters and underscores. This domain has to be unique and cannot be changed. Example of the domain for the mobile app integration: mobileapp. So all files for this integration are in the folder mobileapp/.

CommentsShortcut to: Comments

Want to chat about this article? Just post a message down here. Chat is powered by giscus and all discussions can be found here: TheStaticTurtle/blog-comments