nixos/home-assistant: add customComponents support

Allows passing custom component packages, that get installed into
home-assistant's state directory.
Python depedencies, that are propagated from the custom component
get passed into `extraPackages`, so they are available to
home-assistant at runtime.

This is implemented in a way, that allows coexistence with custom
components not managed through the NixOS module.

Changed files
+95 -2
nixos
modules
services
home-automation
pkgs
servers
home-assistant
custom-components
+38 -2
nixos/modules/services/home-automation/home-assistant.nix
···
# Respect overrides that already exist in the passed package and
# concat it with values passed via the module.
extraComponents = oldArgs.extraComponents or [] ++ extraComponents;
-
extraPackages = ps: (oldArgs.extraPackages or (_: []) ps) ++ (cfg.extraPackages ps);
+
extraPackages = ps: (oldArgs.extraPackages or (_: []) ps)
+
++ (cfg.extraPackages ps)
+
++ (lib.concatMap (component: component.propagatedBuildInputs or []) cfg.customComponents);
}));
# Create a directory that holds all lovelace modules
···
A popular example is `python3Packages.psycopg2`
for PostgreSQL support in the recorder component.
+
'';
+
};
+
+
customComponents = mkOption {
+
type = types.listOf types.package;
+
default = [];
+
example = literalExpression ''
+
with pkgs.home-assistant-custom-components; [
+
prometheus-sensor
+
];
+
'';
+
description = lib.mdDoc ''
+
List of custom component packages to install.
+
+
Available components can be found below `pkgs.home-assistant-custom-components`.
'';
};
···
'' else ''
rm -f "${cfg.configDir}/www/nixos-lovelace-modules"
'';
+
copyCustomComponents = ''
+
mkdir -p "${cfg.configDir}/custom_components"
+
+
# remove components symlinked in from below the /nix/store
+
components="$(find "${cfg.configDir}/custom_components" -maxdepth 1 -type l)"
+
for component in "$components"; do
+
if [[ "$(readlink "$component")" =~ ^${escapeShellArg builtins.storeDir} ]]; then
+
rm "$component"
+
fi
+
done
+
+
# recreate symlinks for desired components
+
declare -a components=(${escapeShellArgs cfg.customComponents})
+
for component in "''${components[@]}"; do
+
path="$(dirname $(find "$component" -name "manifest.json"))"
+
ln -fns "$path" "${cfg.configDir}/custom_components/"
+
done
+
'';
in
(optionalString (cfg.config != null) copyConfig) +
(optionalString (cfg.lovelaceConfig != null) copyLovelaceConfig) +
-
copyCustomLovelaceModules
+
copyCustomLovelaceModules +
+
copyCustomComponents
;
environment.PYTHONPATH = package.pythonPath;
serviceConfig = let
+57
pkgs/servers/home-assistant/custom-components/README.md
···
+
# Packaging guidelines
+
+
## buildHomeAssistantComponent
+
+
Custom components should be packaged using the
+
`buildHomeAssistantComponent` function, that is provided at top-level.
+
It builds upon `buildPythonPackage` but uses a custom install and check
+
phase.
+
+
Python runtime dependencies can be directly consumed as unqualified
+
function arguments. Pass them into `propagatedBuildInputs`, for them to
+
be available to Home Assistant.
+
+
Out-of-tree components need to use python packages from
+
`home-assistant.python.pkgs` as to not introduce conflicting package
+
versions into the Python environment.
+
+
+
**Example Boilerplate:**
+
+
```nix
+
{ lib
+
, buildHomeAssistantcomponent
+
, fetchFromGitHub
+
}:
+
+
buildHomeAssistantComponent {
+
# pname, version
+
+
src = fetchFromGithub {
+
# owner, repo, rev, hash
+
};
+
+
propagatedBuildInputs = [
+
# python requirements, as specified in manifest.json
+
];
+
+
meta = with lib; {
+
# changelog, description, homepage, license, maintainers
+
}
+
}
+
+
## Package name normalization
+
+
Apply the same normalization rules as defined for python packages in
+
[PEP503](https://peps.python.org/pep-0503/#normalized-names).
+
The name should be lowercased and dots, underlines or multiple
+
dashes should all be replaced by a single dash.
+
+
## Manifest check
+
+
The `buildHomeAssistantComponent` builder uses a hook to check whether
+
the dependencies specified in the `manifest.json` are present and
+
inside the specified version range.
+
+
There shouldn't be a need to disable this hook, but you can set
+
`dontCheckManifest` to `true` in the derivation to achieve that.