1{
2 stdenv,
3 lib,
4 callPackage,
5 fetchFromGitHub,
6 fetchPypi,
7 python313,
8 replaceVars,
9 ffmpeg-headless,
10 inetutils,
11 nixosTests,
12 home-assistant,
13 testers,
14
15 # Look up dependencies of specified components in component-packages.nix
16 extraComponents ? [ ],
17
18 # Additional packages to add to propagatedBuildInputs
19 extraPackages ? ps: [ ],
20
21 # Override Python packages using
22 # self: super: { pkg = super.pkg.overridePythonAttrs (oldAttrs: { ... }); }
23 # Applied after defaultOverrides
24 packageOverrides ? self: super: { },
25
26 # Skip pip install of required packages on startup
27 skipPip ? true,
28}:
29
30let
31 defaultOverrides = [
32 # Override the version of some packages pinned in Home Assistant's setup.py and requirements_all.txt
33
34 (self: super: {
35 aioelectricitymaps = super.aioelectricitymaps.overridePythonAttrs (oldAttrs: rec {
36 version = "0.4.0";
37 src = fetchFromGitHub {
38 owner = "jpbede";
39 repo = "aioelectricitymaps";
40 rev = "refs/tags/v${version}";
41 hash = "sha256-q06B40c0uvSuzH/3YCoxg4p9aNIOPrphsoESktF+B14=";
42 };
43 nativeCheckInputs = with self; [
44 aresponses
45 ];
46 });
47
48 aioskybell = super.aioskybell.overridePythonAttrs (oldAttrs: rec {
49 version = "22.7.0";
50 src = fetchFromGitHub {
51 owner = "tkdrob";
52 repo = "aioskybell";
53 rev = "refs/tags/${version}";
54 hash = "sha256-aBT1fDFtq1vasTvCnAXKV2vmZ6LBLZqRCiepv1HDJ+Q=";
55 };
56 });
57
58 aiowatttime = super.aiowatttime.overridePythonAttrs (oldAttrs: rec {
59 version = "0.1.1";
60 src = fetchFromGitHub {
61 owner = "bachya";
62 repo = "aiowatttime";
63 rev = "refs/tags/${version}";
64 hash = "sha256-tWnxGLJT+CRFvkhxFamHxnLXBvoR8tfOvzH1o1i5JJg=";
65 };
66 postPatch = ''
67 substituteInPlace pyproject.toml --replace-fail \
68 '"setuptools >= 35.0.2", "wheel >= 0.29.0", "poetry>=0.12"' \
69 '"poetry-core"'
70 '';
71 });
72
73 astral = super.astral.overridePythonAttrs (oldAttrs: rec {
74 pname = "astral";
75 version = "2.2";
76 src = fetchPypi {
77 inherit pname version;
78 hash = "sha256-5B2ZZ9XEi+QhNGVS8PTe2tQ/85qDV09f8q0ytmJ7b74=";
79 };
80 postPatch = ''
81 substituteInPlace pyproject.toml \
82 --replace-fail "poetry>=1.0.0b1" "poetry-core" \
83 --replace-fail "poetry.masonry" "poetry.core.masonry"
84 '';
85 propagatedBuildInputs = (oldAttrs.propagatedBuildInputs or [ ]) ++ [
86 self.pytz
87 ];
88 });
89
90 async-timeout = super.async-timeout.overridePythonAttrs (oldAttrs: rec {
91 version = "4.0.3";
92 src = fetchFromGitHub {
93 owner = "aio-libs";
94 repo = "async-timeout";
95 tag = "v${version}";
96 hash = "sha256-gJGVRm7YMWnVicz2juHKW8kjJBxn4/vQ/kc2kQyl1i4=";
97 };
98 });
99
100 av = super.av.overridePythonAttrs (rec {
101 version = "13.1.0";
102 src = fetchFromGitHub {
103 owner = "PyAV-Org";
104 repo = "PyAV";
105 tag = "v${version}";
106 hash = "sha256-x2a9SC4uRplC6p0cD7fZcepFpRidbr6JJEEOaGSWl60=";
107 };
108 });
109
110 brother = super.brother.overridePythonAttrs (rec {
111 version = "4.3.1";
112 src = fetchFromGitHub {
113 owner = "bieniu";
114 repo = "brother";
115 tag = version;
116 hash = "sha256-fWa5FNBGV8tnJ3CozMicXLGsDvnTjNzU8PdV266MeeQ=";
117 };
118 });
119
120 google-genai = super.google-genai.overridePythonAttrs (old: rec {
121 version = "1.7.0";
122 src = fetchFromGitHub {
123 owner = "googleapis";
124 repo = "python-genai";
125 tag = "v${version}";
126 hash = "sha256-vmrFPE7H9s9varrP0s6WK4opoU1hREH7rVVjrKiXY5E=";
127 };
128 });
129
130 gspread = super.gspread.overridePythonAttrs (oldAttrs: rec {
131 version = "5.12.4";
132 src = fetchFromGitHub {
133 owner = "burnash";
134 repo = "gspread";
135 rev = "refs/tags/v${version}";
136 hash = "sha256-i+QbnF0Y/kUMvt91Wzb8wseO/1rZn9xzeA5BWg1haks=";
137 };
138 dependencies = with self; [
139 requests
140 ];
141 });
142
143 mcp = super.mcp.overridePythonAttrs (oldAttrs: rec {
144 version = "1.5.0";
145 src = fetchFromGitHub {
146 inherit (oldAttrs.src) owner repo;
147 tag = "v${version}";
148 hash = "sha256-Z2NN6k4mD6NixDON1MUOELpBZW9JvMvFErcCbFPdg2o=";
149 };
150 });
151
152 notifications-android-tv = super.notifications-android-tv.overridePythonAttrs (oldAttrs: rec {
153 version = "0.1.5";
154 format = "setuptools";
155
156 src = fetchFromGitHub {
157 owner = "engrbm87";
158 repo = "notifications_android_tv";
159 rev = "refs/tags/${version}";
160 hash = "sha256-adkcUuPl0jdJjkBINCTW4Kmc16C/HzL+jaRZB/Qr09A=";
161 };
162
163 nativeBuildInputs = with self; [
164 setuptools
165 ];
166
167 propagatedBuildInputs = with self; [
168 requests
169 ];
170
171 doCheck = false; # no tests
172 });
173
174 openhomedevice = super.openhomedevice.overridePythonAttrs (oldAttrs: rec {
175 version = "2.2";
176 src = fetchFromGitHub {
177 inherit (oldAttrs.src) owner repo;
178 rev = "refs/tags/${version}";
179 hash = "sha256-GGp7nKFH01m1KW6yMkKlAdd26bDi8JDWva6OQ0CWMIw=";
180 };
181 });
182
183 plexapi = super.plexapi.overrideAttrs (oldAttrs: rec {
184 version = "4.15.16";
185 src = fetchFromGitHub {
186 owner = "pkkid";
187 repo = "python-plexapi";
188 tag = version;
189 hash = "sha256-NwGGNN6LC3gvE8zoVL5meNWMbqZjJ+6PcU2ebJTfJmU=";
190 };
191 });
192
193 # Pinned due to API changes in 0.1.0
194 poolsense = super.poolsense.overridePythonAttrs (oldAttrs: rec {
195 version = "0.0.8";
196 src = fetchPypi {
197 pname = "poolsense";
198 inherit version;
199 hash = "sha256-17MHrYRmqkH+1QLtgq2d6zaRtqvb9ju9dvPt9gB2xCc=";
200 };
201 });
202
203 # Pinned due to API changes >0.3.5.3
204 pyatag = super.pyatag.overridePythonAttrs (oldAttrs: rec {
205 version = "0.3.5.3";
206 src = fetchFromGitHub {
207 owner = "MatsNl";
208 repo = "pyatag";
209 rev = version;
210 sha256 = "00ly4injmgrj34p0lyx7cz2crgnfcijmzc0540gf7hpwha0marf6";
211 };
212 });
213
214 pydexcom = super.pydexcom.overridePythonAttrs (oldAttrs: rec {
215 version = "0.2.3";
216 src = fetchFromGitHub {
217 owner = "gagebenne";
218 repo = "pydexcom";
219 rev = "refs/tags/${version}";
220 hash = "sha256-ItDGnUUUTwCz4ZJtFVlMYjjoBPn2h8QZgLzgnV2T/Qk=";
221 };
222 });
223
224 pyflume = super.pyflume.overridePythonAttrs (oldAttrs: rec {
225 version = "0.6.5";
226 src = fetchFromGitHub {
227 owner = "ChrisMandich";
228 repo = "PyFlume";
229 rev = "refs/tags/v${version}";
230 hash = "sha256-kIE3y/qlsO9Y1MjEQcX0pfaBeIzCCHk4f1Xa215BBHo=";
231 };
232 dependencies = oldAttrs.propagatedBuildInputs or [ ] ++ [
233 self.pytz
234 ];
235 });
236
237 pykaleidescape = super.pykaleidescape.overridePythonAttrs (oldAttrs: rec {
238 version = "1.0.1";
239 src = fetchFromGitHub {
240 inherit (oldAttrs.src) owner repo;
241 rev = "refs/tags/v${version}";
242 hash = "sha256-KM/gtpsQ27QZz2uI1t/yVN5no0zp9LZag1duAJzK55g=";
243 };
244 });
245
246 pyoctoprintapi = super.pyoctoprintapi.overridePythonAttrs (oldAttrs: rec {
247 version = "0.1.12";
248 src = fetchFromGitHub {
249 owner = "rfleming71";
250 repo = "pyoctoprintapi";
251 rev = "refs/tags/v${version}";
252 hash = "sha256-Jf/zYnBHVl3TYxFy9Chy6qNH/eCroZkmUOEWfd62RIo=";
253 };
254 });
255
256 # snmp component does not support pysnmp 7.0+
257 pysnmp = super.pysnmp.overridePythonAttrs (oldAttrs: rec {
258 version = "6.2.6";
259 src = fetchFromGitHub {
260 owner = "lextudio";
261 repo = "pysnmp";
262 tag = "v${version}";
263 hash = "sha256-+FfXvsfn8XzliaGUKZlzqbozoo6vDxUkgC87JOoVasY=";
264 };
265 });
266
267 pysnmpcrypto = super.pysnmpcrypto.overridePythonAttrs (oldAttrs: rec {
268 version = "0.0.4";
269 src = fetchFromGitHub {
270 owner = "lextudio";
271 repo = "pysnmpcrypto";
272 tag = "v${version}";
273 hash = "sha256-f0w4Nucpe+5VE6nhlnePRH95AnGitXeT3BZb3dhBOTk=";
274 };
275 build-system = with self; [ setuptools ];
276 postPatch = ''
277 # ValueError: invalid literal for int() with base 10: 'post0' in File "<string>", line 104, in <listcomp>
278 substituteInPlace setup.py --replace \
279 "observed_version = [int(x) for x in setuptools.__version__.split('.')]" \
280 "observed_version = [36, 2, 0]"
281 '';
282 });
283
284 pysnooz = super.pysnooz.overridePythonAttrs (oldAttrs: rec {
285 version = "0.8.6";
286 src = fetchFromGitHub {
287 owner = "AustinBrunkhorst";
288 repo = "pysnooz";
289 rev = "refs/tags/v${version}";
290 hash = "sha256-hJwIObiuFEAVhgZXYB9VCeAlewBBnk0oMkP83MUCpyU=";
291 };
292 });
293
294 pytradfri = super.pytradfri.overridePythonAttrs (oldAttrs: rec {
295 version = "9.0.1";
296 src = fetchFromGitHub {
297 owner = "home-assistant-libs";
298 repo = "pytradfri";
299 rev = "refs/tags/${version}";
300 hash = "sha256-xOdTzG0bF5p1QpkXv2btwrVugQRjSwdAj8bXcC0IoQg=";
301 };
302 patches = [ ];
303 doCheck = false;
304 });
305
306 # Pinned due to API changes ~1.0
307 vultr = super.vultr.overridePythonAttrs (oldAttrs: rec {
308 version = "0.1.2";
309 src = fetchFromGitHub {
310 owner = "spry-group";
311 repo = "python-vultr";
312 rev = version;
313 hash = "sha256-sHCZ8Csxs5rwg1ZG++hP3MfK7ldeAdqm5ta9tEXeW+I=";
314 };
315 });
316
317 wolf-comm = super.wolf-comm.overridePythonAttrs (rec {
318 version = "0.0.23";
319 src = fetchFromGitHub {
320 owner = "janrothkegel";
321 repo = "wolf-comm";
322 tag = version;
323 hash = "sha256-LpehooW3vmohiyMwOQTFNLiNCsaLKelWQxQk8bl+y1k=";
324 };
325 });
326
327 # internal python packages only consumed by home-assistant itself
328 hass-web-proxy-lib = self.callPackage ./python-modules/hass-web-proxy-lib { };
329 home-assistant-frontend = self.callPackage ./frontend.nix { };
330 home-assistant-intents = self.callPackage ./intents.nix { };
331 homeassistant = self.toPythonModule home-assistant;
332 pytest-homeassistant-custom-component =
333 self.callPackage ./pytest-homeassistant-custom-component.nix
334 { };
335 })
336 ];
337
338 python = python313.override {
339 self = python;
340 packageOverrides = lib.composeManyExtensions (defaultOverrides ++ [ packageOverrides ]);
341 };
342
343 componentPackages = import ./component-packages.nix;
344
345 availableComponents = builtins.attrNames componentPackages.components;
346
347 inherit (componentPackages) supportedComponentsWithTests;
348
349 getPackages = component: componentPackages.components.${component};
350
351 componentBuildInputs = lib.concatMap (component: getPackages component python.pkgs) extraComponents;
352
353 # Ensure that we are using a consistent package set
354 extraBuildInputs = extraPackages python.pkgs;
355
356 # Don't forget to run update-component-packages.py after updating
357 hassVersion = "2025.7.4";
358
359in
360python.pkgs.buildPythonApplication rec {
361 pname = "homeassistant";
362 version =
363 assert (componentPackages.version == hassVersion);
364 hassVersion;
365 pyproject = true;
366
367 # check REQUIRED_PYTHON_VER in homeassistant/const.py
368 disabled = python.pythonOlder "3.13";
369
370 # don't try and fail to strip 6600+ python files, it takes minutes!
371 dontStrip = true;
372
373 # Primary source is the git, which has the tests and allows bisecting the core
374 src = fetchFromGitHub {
375 owner = "home-assistant";
376 repo = "core";
377 tag = version;
378 hash = "sha256-2seMh1trP3PYnuQmWadTAiAPaI+v45+uzn9xkgUuGNE=";
379 };
380
381 # Secondary source is pypi sdist for translations
382 sdist = fetchPypi {
383 inherit pname version;
384 hash = "sha256-KwiwgQ8gAMlHLzpuYYdcLXabVrukhnfFlaODyFpuF88=";
385 };
386
387 build-system = with python.pkgs; [
388 setuptools
389 ];
390
391 pythonRelaxDeps = true;
392
393 # extract translations from pypi sdist
394 prePatch = ''
395 tar --extract --gzip --file $sdist --strip-components 1 --wildcards "**/translations"
396 '';
397
398 # leave this in, so users don't have to constantly update their downstream patch handling
399 patches = [
400 # Follow symlinks in /var/lib/hass/www
401 ./patches/static-follow-symlinks.patch
402
403 # Copy default blueprints without preserving permissions
404 ./patches/default-blueprint-permissions.patch
405
406 # Patch path to ffmpeg binary
407 (replaceVars ./patches/ffmpeg-path.patch {
408 ffmpeg = "${lib.getExe ffmpeg-headless}";
409 })
410 ];
411
412 postPatch = ''
413 substituteInPlace tests/test_core_config.py --replace-fail '"/usr"' "\"$NIX_BUILD_TOP/media\""
414
415 substituteInPlace pyproject.toml \
416 --replace-fail "setuptools==78.1.1" setuptools
417 '';
418
419 dependencies = with python.pkgs; [
420 # Only packages required in pyproject.toml
421 aiodns
422 aiofiles
423 aiohasupervisor
424 aiohttp
425 aiohttp-asyncmdnsresolver
426 aiohttp-cors
427 aiohttp-fast-zlib
428 aiozoneinfo
429 annotatedyaml
430 astral
431 async-interrupt
432 atomicwrites-homeassistant
433 attrs
434 audioop-lts
435 awesomeversion
436 bcrypt
437 certifi
438 ciso8601
439 cronsim
440 cryptography
441 fnv-hash-fast
442 ha-ffmpeg
443 hass-nabucasa
444 hassil
445 home-assistant-bluetooth
446 home-assistant-intents
447 httpx
448 ifaddr
449 jinja2
450 lru-dict
451 mutagen
452 numpy
453 orjson
454 packaging
455 pillow
456 propcache
457 psutil-home-assistant
458 pyjwt
459 pymicro-vad
460 pyopenssl
461 pyspeex-noise
462 python-slugify
463 pyturbojpeg
464 pyyaml
465 requests
466 securetar
467 sqlalchemy
468 standard-aifc
469 standard-telnetlib
470 typing-extensions
471 ulid-transform
472 urllib3
473 uv
474 voluptuous
475 voluptuous-openapi
476 voluptuous-serialize
477 webrtc-models
478 yarl
479 zeroconf
480 # REQUIREMENTS in homeassistant/auth/mfa_modules/totp.py and homeassistant/auth/mfa_modules/notify.py
481 pyotp
482 pyqrcode
483 ];
484
485 makeWrapperArgs = lib.optional skipPip "--add-flags --skip-pip";
486
487 # upstream only tests on Linux, so do we.
488 doCheck = stdenv.hostPlatform.isLinux;
489
490 nativeCheckInputs =
491 with python.pkgs;
492 [
493 # test infrastructure (selectively from requirement_test.txt)
494 freezegun
495 pytest-asyncio
496 pytest-aiohttp
497 pytest-freezer
498 pytest-mock
499 pytest-socket
500 pytest-timeout
501 pytest-unordered
502 pytest-xdist
503 pytestCheckHook
504 requests-mock
505 respx
506 syrupy
507 tomli
508 # Sneakily imported in tests/conftest.py
509 paho-mqtt
510 # Used in tests/non_packaged_scripts/test_alexa_locales.py
511 beautifulsoup4
512 ]
513 ++ lib.concatMap (component: getPackages component python.pkgs) [
514 # some components are needed even if tests in tests/components are disabled
515 "default_config"
516 "hue"
517 "qwikswitch"
518 ];
519
520 pytestFlags = [
521 # assign tests grouped by file to workers
522 "--dist=loadfile"
523 # enable full variable printing on error
524 "--showlocals"
525 ];
526
527 enabledTestPaths = [
528 # tests are located in tests/
529 "tests"
530 ];
531
532 disabledTestPaths = [
533 # we neither run nor distribute hassfest
534 "tests/hassfest"
535 # we don't care about code quality
536 "tests/pylint"
537 # redundant component import test, which would make debugpy & sentry expensive to review
538 "tests/test_circular_imports.py"
539 # don't bulk test all components
540 "tests/components"
541 # AssertionError: assert 1 == 0
542 "tests/test_config.py::test_merge"
543 # checks whether pip is installed
544 "tests/util/test_package.py::test_check_package_fragment"
545 # flaky
546 "tests/test_bootstrap.py::test_setup_hass_takes_longer_than_log_slow_startup"
547 "tests/test_test_fixtures.py::test_evict_faked_translations"
548 "tests/helpers/test_backup.py::test_async_get_manager"
549 # (2025.7.0) Fails to find name of tracked time interval in scheduled jobs
550 "tests/helpers/test_event.py::test_track_time_interval_name"
551 # (2025.7.2) Exception string mismatch (non-blocking vs non blocking)
552 "tests/test_core.py::test_services_call_return_response_requires_blocking"
553 ];
554
555 preCheck = ''
556 export HOME="$TEMPDIR"
557
558 # the tests require the existance of a media dir
559 mkdir "$NIX_BUILD_TOP"/media
560
561 # put ping binary into PATH, e.g. for wake_on_lan tests
562 export PATH=${inetutils}/bin:$PATH
563 '';
564
565 passthru = {
566 inherit
567 availableComponents
568 extraComponents
569 getPackages
570 python
571 supportedComponentsWithTests
572 ;
573 pythonPath = python.pkgs.makePythonPath (componentBuildInputs ++ extraBuildInputs);
574 frontend = python.pkgs.home-assistant-frontend;
575 intents = python.pkgs.home-assistant-intents;
576 tests = {
577 nixos = nixosTests.home-assistant;
578 components = callPackage ./tests.nix { };
579 version = testers.testVersion {
580 package = home-assistant;
581 command = "hass --version";
582 };
583 withoutCheckDeps = home-assistant.overridePythonAttrs {
584 pname = "home-assistant-without-check-deps";
585 doCheck = false;
586 };
587 };
588 };
589
590 meta = with lib; {
591 homepage = "https://home-assistant.io/";
592 changelog = "https://github.com/home-assistant/core/releases/tag/${src.tag}";
593 description = "Open source home automation that puts local control and privacy first";
594 license = licenses.asl20;
595 teams = [ teams.home-assistant ];
596 platforms = platforms.linux;
597 mainProgram = "hass";
598 };
599}