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  "domain": "open433",
3  "name": "Open433 board",
4  "documentation": "https://github.com/TheStaticTurtle/Open433",
5  "requirements": ["pyserial==3.4"],
6  "codeowners": ["@TheStaticTurtle"]
7}

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:

1import logging
2from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP
3import homeassistant.helpers.config_validation as cv
4import voluptuous as vol
5import threading
6from . 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_LOGGER = logging.getLogger(__name__)
 2
 3DOMAIN = "open433"
 4
 5CONF_COMPORT = "port"
 6CONF_COMSPEED = "speed"
 7
 8REQ_LOCK = threading.Lock()
 9CONFIG_SCHEMA = vol.Schema(
10	{
11		DOMAIN: vol.Schema({
12			vol.Required(CONF_COMPORT): cv.string,
13			vol.Optional(CONF_COMSPEED, default=9600): cv.positive_int,
14		})
15	},
16	extra=vol.ALLOW_EXTRA,
17)

So for me the configuration could look like this:

1open433:
2  port: COM3

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

 1def setup(hass, config):
 2	conf = config[DOMAIN]
 3	comport = conf.get(CONF_COMPORT)
 4	comspeed = conf.get(CONF_COMSPEED)
 5
 6	rf = rcswitch.RCSwitch(comport, speed=comspeed)
 7	rf.libWaitForAck(True, timeout=1)
 8
 9	def cleanup(event):
10		rf.cleanup()
11
12	def prepare(event):
13		rf.prepare()
14		rf.startReceivingThread()
15		hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, cleanup)
16
17	hass.bus.listen_once(EVENT_HOMEASSISTANT_START, prepare)
18	hass.data[DOMAIN] = rf
19
20	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

 1import logging
 2
 3import voluptuous as vol
 4
 5from homeassistant.components.switch import SwitchEntity, PLATFORM_SCHEMA
 6from homeassistant.const import CONF_NAME, CONF_SWITCHES
 7import homeassistant.helpers.config_validation as cv
 8from . import DOMAIN, REQ_LOCK, rcswitch
 9
10_LOGGER = logging.getLogger(__name__)
11
12CONF_CODE_OFF = "code_off"
13CONF_CODE_ON = "code_on"
14CONF_PROTOCOL = "protocol"
15CONF_LENGTH = "length"
16CONF_SIGNAL_REPETITIONS = "signal_repetitions"
17CONF_ENABLE_RECEIVE = "enable_receive"
18
19SWITCH_SCHEMA = vol.Schema(
20	{
21		vol.Required(CONF_CODE_OFF): vol.All(cv.ensure_list_csv, [cv.positive_int]),
22		vol.Required(CONF_CODE_ON): vol.All(cv.ensure_list_csv, [cv.positive_int]),
23		vol.Optional(CONF_LENGTH, default=32): cv.positive_int,
24		vol.Optional(CONF_SIGNAL_REPETITIONS, default=15): cv.positive_int,
25		vol.Optional(CONF_PROTOCOL, default=2): cv.positive_int,
26		vol.Optional(CONF_ENABLE_RECEIVE, default=False): cv.boolean,
27	}
28)
29
30PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
31	{
32		vol.Required(CONF_SWITCHES): vol.Schema({cv.string: SWITCH_SCHEMA}),
33	}
34)

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)

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

Then you need to set up the platform

 1def setup_platform(hass, config, add_entities, discovery_info=None):
 2	rf = hass.data[DOMAIN]
 3
 4	switches = config.get(CONF_SWITCHES)
 5	devices = []
 6	for dev_name, properties in switches.items():
 7		devices.append(
 8			Open433Switch(
 9				properties.get(CONF_NAME, dev_name),
10				rf,
11				properties.get(CONF_PROTOCOL),
12				properties.get(CONF_LENGTH),
13				properties.get(CONF_SIGNAL_REPETITIONS),
14				properties.get(CONF_CODE_ON),
15				properties.get(CONF_CODE_OFF),
16				properties.get(CONF_ENABLE_RECEIVE),
17			)
18		)
19
20	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:

 1class Open433Switch(SwitchEntity):
 2	def __init__(self, name, rf, protocol, length, signal_repetitions, code_on, code_off,enable_rx):
 3		self._name = name
 4		self._state = False
 5		self._rf = rf
 6		self._protocol = protocol
 7		self._length = length
 8		self._code_on = code_on
 9		self._code_off = code_off
10		self._signal_repetitions = signal_repetitions
11		if enable_rx:
12			self._rf.addIncomingPacketListener(self._incoming)
13
14	def _incoming(self, packet):
15		if isinstance(packet, rcswitch.packets.ReceivedSignal):
16			if packet.length == self._length and packet.protocol == self._protocol:
17				if packet.decimal in self._code_on:
18					self._state = True
19					self.schedule_update_ha_state()
20				if packet.decimal in self._code_off:
21					self._state = False
22					self.schedule_update_ha_state()
23
24	@property
25	def should_poll(self):
26		return False
27
28	@property
29	def name(self):
30		return self._name
31
32	@property
33	def is_on(self):
34		return self._state
35
36	def _send_code(self, code_list, protocol, length):
37		with REQ_LOCK:
38			self._rf.setRepeatTransmit(self._signal_repetitions)
39			for code in code_list:
40				packet = rcswitch.packets.SendDecimal(value=code, length=length, protocol=protocol, delay=700)
41				self._rf.send(packet)
42		return True
43
44	def turn_on(self, **kwargs):
45		if self._send_code(self._code_on, self._protocol, self._length):
46			self._state = True
47			self.schedule_update_ha_state()
48
49	def turn_off(self, **kwargs):
50		if self._send_code(self._code_off, self._protocol, self._length):
51			self._state = False
52			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