1 import ./make-test-python.nix ({ pkgs, lib, ... }:
4 configDir = "/var/lib/foobar";
6 name = "home-assistant";
7 meta.maintainers = lib.teams.home-assistant.members;
9 nodes.hass = { pkgs, ... }: {
10 services.postgresql = {
12 ensureDatabases = [ "hass" ];
15 ensureDBOwnership = true;
19 services.home-assistant = {
23 # provide dependencies through package overrides
24 package = (pkgs.home-assistant.override {
25 extraPackages = ps: with ps; [
29 # test char-tty device allow propagation into the service
34 # provide component dependencies explicitly from the module
39 # provide package for postgresql support
40 extraPackages = python3Packages: with python3Packages; [
44 # test loading custom components
45 customComponents = with pkgs.home-assistant-custom-components; [
47 # tests loading multiple components from a single package
51 # test loading lovelace modules
52 customLovelaceModules = with pkgs.home-assistant-custom-lovelace-modules; [
65 # configure the recorder component to use the postgresql db
66 recorder.db_url = "postgresql://@/hass";
68 # we can't load default_config, because the updater requires
69 # network access and would cause an error, so load frontend
71 # https://www.home-assistant.io/integrations/frontend/
74 # include some popular integrations, that absolutely shouldn't break
79 # set up a wake-on-lan switch to test capset capability required
80 # for the ping suid wrapper
81 # https://www.home-assistant.io/integrations/wake_on_lan/
83 platform = "wake_on_lan";
84 mac = "00:11:22:33:44:55";
88 # test component-based capability assignment (CAP_NET_BIND_SERVICE)
89 # https://www.home-assistant.io/integrations/emulated_hue/
91 host_ip = "127.0.0.1";
95 # https://www.home-assistant.io/integrations/logger/
101 # configure the sample lovelace dashboard
103 title = "My Awesome Home";
109 content = "Welcome to your **Lovelace UI**.";
113 lovelaceConfigWritable = true;
116 # Cause a configuration change inside `configuration.yml` and verify that the process is being reloaded.
117 specialisation.differentName = {
118 inheritParentConfig = true;
119 configuration.services.home-assistant.config.homeassistant.name = lib.mkForce "Test Home";
122 # Cause a configuration change that requires a service restart as we added a new runtime dependency
123 specialisation.newFeature = {
124 inheritParentConfig = true;
125 configuration.services.home-assistant.config.backup = {};
128 specialisation.removeCustomThings = {
129 inheritParentConfig = true;
130 configuration.services.home-assistant = {
131 customComponents = lib.mkForce [];
132 customLovelaceModules = lib.mkForce [];
137 testScript = { nodes, ... }: let
138 system = nodes.hass.system.build.toplevel;
146 def get_journal_cursor() -> str:
147 exit, out = hass.execute("journalctl -u home-assistant.service -n1 -o json-pretty --output-fields=__CURSOR")
149 return json.loads(out)["__CURSOR"]
152 def get_journal_since(cursor) -> str:
153 exit, out = hass.execute(f"journalctl --after-cursor='{cursor}' -u home-assistant.service")
158 def get_unit_property(property) -> str:
159 exit, out = hass.execute(f"systemctl show --property={property} home-assistant.service")
164 def wait_for_homeassistant(cursor):
165 hass.wait_until_succeeds(f"journalctl --after-cursor='{cursor}' -u home-assistant.service | grep -q 'Home Assistant initialized in'")
168 hass.wait_for_unit("home-assistant.service")
169 cursor = get_journal_cursor()
171 with subtest("Check that YAML configuration file is in place"):
172 hass.succeed("test -L ${configDir}/configuration.yaml")
174 with subtest("Check the lovelace config is copied because lovelaceConfigWritable = true"):
175 hass.succeed("test -f ${configDir}/ui-lovelace.yaml")
177 with subtest("Check that Home Assistant's web interface and API can be reached"):
178 wait_for_homeassistant(cursor)
179 hass.wait_for_open_port(8123)
180 hass.succeed("curl --fail http://localhost:8123/lovelace")
182 with subtest("Check that custom components get installed"):
183 hass.succeed("test -f ${configDir}/custom_components/prometheus_sensor/manifest.json")
184 for integration in ("prometheus_sensor", "spook", "spook_inverse"):
185 hass.wait_until_succeeds(f"journalctl -u home-assistant.service | grep -q 'We found a custom integration {integration} which has not been tested by Home Assistant'")
187 with subtest("Check that lovelace modules are referenced and fetchable"):
188 hass.succeed("grep -q 'mini-graph-card-bundle.js' '${configDir}/configuration.yaml'")
189 hass.succeed("curl --fail http://localhost:8123/local/nixos-lovelace-modules/mini-graph-card-bundle.js")
191 with subtest("Check that optional dependencies are in the PYTHONPATH"):
192 env = get_unit_property("Environment")
193 python_path = env.split("PYTHONPATH=")[1].split()[0]
194 for package in ["colorama", "paho-mqtt", "psycopg2"]:
195 assert package in python_path, f"{package} not in PYTHONPATH"
197 with subtest("Check that declaratively configured components get setup"):
198 journal = get_journal_since(cursor)
199 for domain in ["emulated_hue", "wake_on_lan"]:
200 assert f"Setup of domain {domain} took" in journal, f"{domain} setup missing"
202 with subtest("Check that capabilities are passed for emulated_hue to bind to port 80"):
203 hass.wait_for_open_port(80)
204 hass.succeed("curl --fail http://localhost:80/description.xml")
206 with subtest("Check extra components are considered in systemd unit hardening"):
207 hass.succeed("systemctl show -p DeviceAllow home-assistant.service | grep -q char-ttyUSB")
209 with subtest("Check service reloads when configuration changes"):
210 pid = hass.succeed("systemctl show --property=MainPID home-assistant.service")
211 cursor = get_journal_cursor()
212 hass.succeed("${system}/specialisation/differentName/bin/switch-to-configuration test")
213 new_pid = hass.succeed("systemctl show --property=MainPID home-assistant.service")
214 assert pid == new_pid, "The PID of the process should not change between process reloads"
215 wait_for_homeassistant(cursor)
217 with subtest("Check service restarts when dependencies change"):
219 cursor = get_journal_cursor()
220 hass.succeed("${system}/specialisation/newFeature/bin/switch-to-configuration test")
221 new_pid = hass.succeed("systemctl show --property=MainPID home-assistant.service")
222 assert pid != new_pid, "The PID of the process should change when its PYTHONPATH changess"
223 wait_for_homeassistant(cursor)
225 with subtest("Check that new components get setup after restart"):
226 journal = get_journal_since(cursor)
227 for domain in ["backup"]:
228 assert f"Setup of domain {domain} took" in journal, f"{domain} setup missing"
230 with subtest("Check custom components and custom lovelace modules get removed"):
231 cursor = get_journal_cursor()
232 hass.succeed("${system}/specialisation/removeCustomThings/bin/switch-to-configuration test")
233 hass.fail("grep -q 'mini-graph-card-bundle.js' '${configDir}/ui-lovelace.yaml'")
234 for integration in ("prometheus_sensor", "spook", "spook_inverse"):
235 hass.fail(f"test -f ${configDir}/custom_components/{integration}/manifest.json")
236 wait_for_homeassistant(cursor)
238 with subtest("Check that no errors were logged"):
239 hass.fail("journalctl -u home-assistant -o cat | grep -q ERROR")
241 with subtest("Check systemd unit hardening"):
242 hass.log(hass.succeed("systemctl cat home-assistant.service"))
243 hass.log(hass.succeed("systemd-analyze security home-assistant.service"))