infrastructure/modules/nixos/home-assistant.nix
2024-05-08 11:33:46 -07:00

283 lines
8.3 KiB
Nix

{
pkgs,
config,
lib,
...
}: let
cfg = config.services.home-assistant;
inherit (lib.modules) mkIf mkMerge mkBefore mkDefault mkOptionDefault;
inherit (lib.options) mkOption mkEnableOption;
inherit (lib.lists) optional elem unique;
inherit (lib.strings) toLower;
in {
options.services.home-assistant = with lib.types; {
mutableUiConfig = mkEnableOption "UI-editable config files";
domain = mkOption {
type = str;
default = config.networking.domain;
};
secretsFile = mkOption {
type = nullOr path;
default = null;
};
homekit = {
enable =
mkEnableOption "homekit"
// {
default = cfg.config.homekit or [] != [];
};
openFirewall =
mkEnableOption "homekit ports"
// {
default = cfg.openFirewall;
};
};
googleAssistant.enable =
mkEnableOption "Google Assistant"
// {
default = cfg.config.google_assistant or {} != {};
};
androidTv.enable =
mkEnableOption "Android TV"
// {
default = elem "androidtv" cfg.extraComponents;
};
brother.enable =
mkEnableOption "brother"
// {
default = elem "brother" cfg.extraComponents;
};
cast = {
enable =
mkEnableOption "Chromecast"
// {
default = elem "cast" cfg.extraComponents;
};
openFirewall =
mkEnableOption "Chromecast ports"
// {
default = cfg.openFirewall;
};
};
finalPackage = mkOption {
type = types.path;
readOnly = true;
};
};
config = {
networking.firewall = let
homekitTcp = mkIf cfg.homekit.enable (
map ({port, ...}: port) cfg.config.homekit or []
);
castUdpRanges = mkIf cfg.cast.enable [
{
from = 32768;
to = 60999;
}
];
in
mkIf cfg.enable {
interfaces.local = {
allowedTCPPorts = mkMerge [
(mkIf (!cfg.homekit.openFirewall) homekitTcp)
(mkIf (!cfg.openFirewall) [ cfg.config.http.server_port ])
];
allowedUDPPortRanges = mkIf (!cfg.cast.openFirewall) castUdpRanges;
};
allowedTCPPorts = mkIf cfg.homekit.openFirewall homekitTcp;
allowedUDPPortRanges = mkIf cfg.cast.openFirewall castUdpRanges;
};
# MDNS
services.avahi = mkIf (cfg.enable && cfg.homekit.enable) {
enable = mkDefault true;
publish.enable = let
homekitNames = map (homekit: toLower homekit.name) cfg.config.homekit or [];
in
mkIf (elem config.networking.hostName homekitNames) false;
};
systemd.services.home-assistant = mkIf (cfg.enable && cfg.mutableUiConfig) {
# UI-editable config files
preStart = mkMerge [
(mkBefore ''
touch "${cfg.configDir}/"{automations,scenes,scripts,manual,homekit_entity_config,homekit_include_entities}.yaml
'')
(mkIf (cfg.secretsFile != null) (mkBefore ''
ln -sf ${cfg.secretsFile} "${cfg.configDir}/secrets.yaml"
''))
];
};
};
config.services.home-assistant = {
config = mkMerge [
{
homeassistant = {
external_url = "https://${cfg.domain}";
};
logger = {
default = mkDefault "info";
};
http = {
cors_allowed_origins = [
"https://google.com"
"https://www.home-assistant.io"
];
use_x_forwarded_for = "true";
trusted_proxies = let
inherit (config.networking.access) cidrForNetwork;
in
cidrForNetwork.allLocal.all
++ [
"200::/7"
];
};
recorder = {
db_url = mkIf config.services.postgresql.enable (mkDefault "postgresql://@/hass");
};
counter = {};
device_tracker = {};
energy = {};
group = {};
history = {};
input_boolean = {};
input_button = {};
input_datetime = {};
input_number = {};
input_select = {};
input_text = {};
logbook = {};
schedule = {};
media_source = {};
media_player = [];
mobile_app = {};
my = {};
person = {};
ssdp = {};
switch = {};
stream = {};
sun = {};
system_health = {};
tag = {};
template = {};
timer = {};
webhook = {};
zeroconf = {};
zone = {};
sensor = {};
}
(mkIf cfg.mutableUiConfig {
# https://nixos.wiki/wiki/Home_Assistant#Combine_declarative_and_UI_defined_automations
"automation manual" = [];
"automation ui" = "!include automations.yaml";
# https://nixos.wiki/wiki/Home_Assistant#Combine_declarative_and_UI_defined_scenes
"scene manual" = [];
"scene ui" = "!include scenes.yaml";
"script manual" = [];
"script ui" = "!include scripts.yaml";
})
];
package = let
inherit (cfg.package) python;
# https://github.com/pysnmp/pysnmp/issues/51
needsPyasn1pin =
if lib.versionOlder python.pkgs.pysnmplib.version "6.0"
then true
else lib.warn "pyasn1 pin likely no longer needed" false;
pyasn1prefix = "${python.pkgs.pysnmp-pyasn1}/${python.sitePackages}";
home-assistant = pkgs.home-assistant.override {
packageOverrides = self: super: {
pydantic = let
pydantic2 = self.callPackage (pkgs.path + "/pkgs/development/python-modules/pydantic") { };
in lib.warnIf (lib.versionAtLeast super.pydantic.version "2.0") "home-assistant pydantic override may no longer be needed" pydantic2;
brother = super.brother.overridePythonAttrs (old: {
dontCheckRuntimeDeps =
if old.dontCheckRuntimeDeps or false
then lib.warn "brother override no longer needed" true
else true;
});
mpd2 = super.mpd2.overridePythonAttrs (old: {
patches =
old.patches
or []
++ [
../../packages/mpd2-skip-flaky-test.patch
];
disabledTests = unique (old.disabledTests
or []
++ [
"test_idle_timeout"
]);
});
};
};
in
home-assistant.overrideAttrs (old: {
makeWrapperArgs = old.makeWrapperArgs ++ optional (cfg.brother.enable && needsPyasn1pin) "--prefix PYTHONPATH : ${pyasn1prefix}";
disabledTests = unique (old.disabledTests
or []
++ [
"test_check_config"
]);
});
finalPackage = let
inherit (lib.strings) hasSuffix removeSuffix splitString;
inherit (lib.lists) head;
inherit (lib.attrsets) attrNames filterAttrs;
inherit (config.systemd.services.home-assistant.serviceConfig) ExecStart;
isHassDrv = drv: context: hasSuffix "-${cfg.package.name}.drv" drv && context.outputs or [] == ["out"];
drvs = filterAttrs isHassDrv (builtins.getContext ExecStart);
isImpure = builtins ? currentSystem;
in
mkIf cfg.enable (mkOptionDefault (
if isImpure
then import (head (attrNames drvs))
else removeSuffix "/bin/hass" (head (splitString " " ExecStart))
));
extraPackages = python3Packages:
with python3Packages;
mkMerge [
[
psycopg2
securetar
getmac # for upnp integration
python-otbr-api
(aiogithubapi.overrideAttrs (_: {doInstallCheck = false;}))
]
(mkIf cfg.homekit.enable [
aiohomekit
])
(mkIf cfg.androidTv.enable [
adb-shell
androidtvremote2
])
];
extraComponents = mkMerge [
[
"automation"
"scene"
"script"
"default_config"
"environment_canada"
"met"
"generic_thermostat"
"map"
"mqtt"
"zeroconf"
]
(mkIf cfg.homekit.enable [
"homekit"
])
(mkIf cfg.googleAssistant.enable [
"google"
"google_assistant"
"google_cloud"
])
(map ({platform, ...}: platform) cfg.config.media_player or [])
(map ({platform, ...}: platform) cfg.config.tts or [])
];
};
}