1import ./make-test-python.nix ({ pkgs, lib, ... }:
2
3let
4 configDir = "/var/lib/foobar";
5in {
6 name = "home-assistant";
7 meta.maintainers = lib.teams.home-assistant.members;
8
9 nodes.hass = { pkgs, ... }: {
10 services.postgresql = {
11 enable = true;
12 ensureDatabases = [ "hass" ];
13 ensureUsers = [{
14 name = "hass";
15 ensurePermissions = {
16 "DATABASE hass" = "ALL PRIVILEGES";
17 };
18 }];
19 };
20
21 services.home-assistant = {
22 enable = true;
23 inherit configDir;
24
25 # provide dependencies through package overrides
26 package = (pkgs.home-assistant.override {
27 extraPackages = ps: with ps; [
28 colorama
29 ];
30 extraComponents = [
31 # test char-tty device allow propagation into the service
32 "zha"
33 ];
34 });
35
36 # provide component dependencies explicitly from the module
37 extraComponents = [
38 "mqtt"
39 ];
40
41 # provide package for postgresql support
42 extraPackages = python3Packages: with python3Packages; [
43 psycopg2
44 ];
45
46 config = {
47 homeassistant = {
48 name = "Home";
49 time_zone = "UTC";
50 latitude = "0.0";
51 longitude = "0.0";
52 elevation = 0;
53 };
54
55 # configure the recorder component to use the postgresql db
56 recorder.db_url = "postgresql://@/hass";
57
58 # we can't load default_config, because the updater requires
59 # network access and would cause an error, so load frontend
60 # here explicitly.
61 # https://www.home-assistant.io/integrations/frontend/
62 frontend = {};
63
64 # include some popular integrations, that absolutely shouldn't break
65 esphome = {};
66 knx = {};
67 shelly = {};
68 zha = {};
69
70 # set up a wake-on-lan switch to test capset capability required
71 # for the ping suid wrapper
72 # https://www.home-assistant.io/integrations/wake_on_lan/
73 switch = [ {
74 platform = "wake_on_lan";
75 mac = "00:11:22:33:44:55";
76 host = "127.0.0.1";
77 } ];
78
79 # test component-based capability assignment (CAP_NET_BIND_SERVICE)
80 # https://www.home-assistant.io/integrations/emulated_hue/
81 emulated_hue = {
82 host_ip = "127.0.0.1";
83 listen_port = 80;
84 };
85
86 # https://www.home-assistant.io/integrations/logger/
87 logger = {
88 default = "info";
89 };
90 };
91
92 # configure the sample lovelace dashboard
93 lovelaceConfig = {
94 title = "My Awesome Home";
95 views = [{
96 title = "Example";
97 cards = [{
98 type = "markdown";
99 title = "Lovelace";
100 content = "Welcome to your **Lovelace UI**.";
101 }];
102 }];
103 };
104 lovelaceConfigWritable = true;
105 };
106
107 # Cause a configuration change inside `configuration.yml` and verify that the process is being reloaded.
108 specialisation.differentName = {
109 inheritParentConfig = true;
110 configuration.services.home-assistant.config.homeassistant.name = lib.mkForce "Test Home";
111 };
112
113 # Cause a configuration change that requires a service restart as we added a new runtime dependency
114 specialisation.newFeature = {
115 inheritParentConfig = true;
116 configuration.services.home-assistant.config.backup = {};
117 };
118 };
119
120 testScript = { nodes, ... }: let
121 system = nodes.hass.system.build.toplevel;
122 in
123 ''
124 import json
125
126 start_all()
127
128
129 def get_journal_cursor() -> str:
130 exit, out = hass.execute("journalctl -u home-assistant.service -n1 -o json-pretty --output-fields=__CURSOR")
131 assert exit == 0
132 return json.loads(out)["__CURSOR"]
133
134
135 def get_journal_since(cursor) -> str:
136 exit, out = hass.execute(f"journalctl --after-cursor='{cursor}' -u home-assistant.service")
137 assert exit == 0
138 return out
139
140
141 def get_unit_property(property) -> str:
142 exit, out = hass.execute(f"systemctl show --property={property} home-assistant.service")
143 assert exit == 0
144 return out
145
146
147 def wait_for_homeassistant(cursor):
148 hass.wait_until_succeeds(f"journalctl --after-cursor='{cursor}' -u home-assistant.service | grep -q 'Home Assistant initialized in'")
149
150
151 hass.wait_for_unit("home-assistant.service")
152 cursor = get_journal_cursor()
153
154 with subtest("Check that YAML configuration file is in place"):
155 hass.succeed("test -L ${configDir}/configuration.yaml")
156
157 with subtest("Check the lovelace config is copied because lovelaceConfigWritable = true"):
158 hass.succeed("test -f ${configDir}/ui-lovelace.yaml")
159
160 with subtest("Check that Home Assistant's web interface and API can be reached"):
161 wait_for_homeassistant(cursor)
162 hass.wait_for_open_port(8123)
163 hass.succeed("curl --fail http://localhost:8123/lovelace")
164
165 with subtest("Check that optional dependencies are in the PYTHONPATH"):
166 env = get_unit_property("Environment")
167 python_path = env.split("PYTHONPATH=")[1].split()[0]
168 for package in ["colorama", "paho-mqtt", "psycopg2"]:
169 assert package in python_path, f"{package} not in PYTHONPATH"
170
171 with subtest("Check that declaratively configured components get setup"):
172 journal = get_journal_since(cursor)
173 for domain in ["emulated_hue", "wake_on_lan"]:
174 assert f"Setup of domain {domain} took" in journal, f"{domain} setup missing"
175
176 with subtest("Check that capabilities are passed for emulated_hue to bind to port 80"):
177 hass.wait_for_open_port(80)
178 hass.succeed("curl --fail http://localhost:80/description.xml")
179
180 with subtest("Check extra components are considered in systemd unit hardening"):
181 hass.succeed("systemctl show -p DeviceAllow home-assistant.service | grep -q char-ttyUSB")
182
183 with subtest("Check service reloads when configuration changes"):
184 pid = hass.succeed("systemctl show --property=MainPID home-assistant.service")
185 cursor = get_journal_cursor()
186 hass.succeed("${system}/specialisation/differentName/bin/switch-to-configuration test")
187 new_pid = hass.succeed("systemctl show --property=MainPID home-assistant.service")
188 assert pid == new_pid, "The PID of the process should not change between process reloads"
189 wait_for_homeassistant(cursor)
190
191 with subtest("Check service restarts when dependencies change"):
192 pid = new_pid
193 cursor = get_journal_cursor()
194 hass.succeed("${system}/specialisation/newFeature/bin/switch-to-configuration test")
195 new_pid = hass.succeed("systemctl show --property=MainPID home-assistant.service")
196 assert pid != new_pid, "The PID of the process should change when its PYTHONPATH changess"
197 wait_for_homeassistant(cursor)
198
199 with subtest("Check that new components get setup after restart"):
200 journal = get_journal_since(cursor)
201 for domain in ["esphome"]:
202 assert f"Setup of domain {domain} took" in journal, f"{domain} setup missing"
203
204 with subtest("Check that no errors were logged"):
205 hass.fail("journalctl -u home-assistant -o cat | grep -q ERROR")
206
207 with subtest("Check systemd unit hardening"):
208 hass.log(hass.succeed("systemctl cat home-assistant.service"))
209 hass.log(hass.succeed("systemd-analyze security home-assistant.service"))
210 '';
211})