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