notes: 2.3.0 -> 2.3.1 (#352950)
[NixPkgs.git] / nixos / tests / home-assistant.nix
blob47902fa4e1340bfde8f041b94e502a185c967156
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         # tests loading multiple components from a single package
48         spook
49       ];
51       # test loading lovelace modules
52       customLovelaceModules = with pkgs.home-assistant-custom-lovelace-modules; [
53         mini-graph-card
54       ];
56       config = {
57         homeassistant = {
58           name = "Home";
59           time_zone = "UTC";
60           latitude = "0.0";
61           longitude = "0.0";
62           elevation = 0;
63         };
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
70         # here explicitly.
71         # https://www.home-assistant.io/integrations/frontend/
72         frontend = {};
74         # include some popular integrations, that absolutely shouldn't break
75         knx = {};
76         shelly = {};
77         zha = {};
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/
82         switch = [ {
83           platform = "wake_on_lan";
84           mac = "00:11:22:33:44:55";
85           host = "127.0.0.1";
86         } ];
88         # test component-based capability assignment (CAP_NET_BIND_SERVICE)
89         # https://www.home-assistant.io/integrations/emulated_hue/
90         emulated_hue = {
91           host_ip = "127.0.0.1";
92           listen_port = 80;
93         };
95         # https://www.home-assistant.io/integrations/logger/
96         logger = {
97           default = "info";
98         };
99       };
101       # configure the sample lovelace dashboard
102       lovelaceConfig = {
103         title = "My Awesome Home";
104         views = [{
105           title = "Example";
106           cards = [{
107             type = "markdown";
108             title = "Lovelace";
109             content = "Welcome to your **Lovelace UI**.";
110           }];
111         }];
112       };
113       lovelaceConfigWritable = true;
114     };
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";
120     };
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 = {};
126     };
128     specialisation.removeCustomThings = {
129       inheritParentConfig = true;
130       configuration.services.home-assistant = {
131         customComponents = lib.mkForce [];
132         customLovelaceModules = lib.mkForce [];
133       };
134     };
135   };
137   testScript = { nodes, ... }: let
138     system = nodes.hass.system.build.toplevel;
139   in
140   ''
141     import json
143     start_all()
146     def get_journal_cursor() -> str:
147         exit, out = hass.execute("journalctl -u home-assistant.service -n1 -o json-pretty --output-fields=__CURSOR")
148         assert exit == 0
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")
154         assert exit == 0
155         return out
158     def get_unit_property(property) -> str:
159         exit, out = hass.execute(f"systemctl show --property={property} home-assistant.service")
160         assert exit == 0
161         return out
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"):
218         pid = new_pid
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"))
244   '';