{ config, globals, lib, pkgs, ... }: let homeassistantDomain = "home.${globals.domains.personal}"; fritzboxDomain = "fritzbox.${globals.domains.personal}"; in { imports = [ ./hass-modbus/mennekes-amtron-xtra.nix ]; globals.wireguard.proxy-home.hosts.${config.node.name}.firewallRuleForNode.ward-web-proxy.allowedTCPPorts = [ config.services.home-assistant.config.http.server_port ]; environment.persistence."/persist".directories = [ { directory = config.services.home-assistant.configDir; user = "hass"; group = "hass"; mode = "0700"; } ]; globals.services.home-assistant.domain = homeassistantDomain; # globals.monitoring.http.homeassistant = { # url = "https://${homeasisstantDomain}"; # expectedBodyRegex = "homeassistant"; # network = "internet"; # }; services.matter-server = { enable = true; logLevel = "debug"; }; topology.self.services.home-assistant.info = "https://${homeassistantDomain}"; services.home-assistant = { enable = true; extraComponents = [ "esphome" "forecast_solar" "fritzbox" "matter" "met" "mqtt" "ollama" "radio_browser" "shelly" "soundtouch" # Bose SoundTouch "spotify" "wake_word" "webostv" # LG WebOS TV "whisper" "wyoming" "zha" ]; customComponents = with pkgs.home-assistant-custom-components; [ (pkgs.home-assistant.python.pkgs.callPackage ./hass-components/ha-bambulab.nix { }) dwd waste_collection_schedule ]; customLovelaceModules = with pkgs.home-assistant-custom-lovelace-modules; [ (pkgs.callPackage ./hass-lovelace/config-template-card/package.nix { }) (pkgs.callPackage ./hass-lovelace/hui-element/package.nix { }) apexcharts-card bubble-card button-card card-mod clock-weather-card hourly-weather lg-webos-remote-control mini-graph-card multiple-entity-row mushroom weather-card weather-chart-card ]; config = { default_config = { }; http = { server_host = [ "0.0.0.0" ]; server_port = 8123; use_x_forwarded_for = true; trusted_proxies = [ globals.wireguard.proxy-home.hosts.ward-web-proxy.ipv4 ]; }; zha.zigpy_config.source_routing = true; homeassistant = { name = "!secret ha_name"; latitude = "!secret ha_latitude"; longitude = "!secret ha_longitude"; elevation = "!secret ha_elevation"; currency = "EUR"; time_zone = "Europe/Berlin"; unit_system = "metric"; external_url = "https://${homeassistantDomain}"; internal_url = "https://${homeassistantDomain}"; packages.manual = "!include manual.yaml"; }; lovelace.mode = "yaml"; frontend = { themes = "!include_dir_merge_named themes"; }; "automation ui" = "!include automations.yaml"; "scene" = "!include scenes.yaml"; influxdb = { api_version = 2; host = "localhost"; port = "8086"; max_retries = 10; ssl = false; verify_ssl = false; token = "!secret influxdb_token"; organization = "home"; bucket = "hass"; }; waste_collection_schedule = { sources = [ { name = "ics"; args.url = "!secret muell_ics_url"; calendar_title = "Abfalltermine"; customize = [ { type = "Restmüll 2-wöchentlich"; alias = "Restmüll"; } { type = "Papiertonne 4-wöchentlich"; alias = "Papiermüll"; } ]; } ]; }; sensor = [ { platform = "waste_collection_schedule"; name = "restmuell_upcoming"; value_template = "{{value.types|join(\", \")}}|{{value.daysTo}}|{{value.date.strftime(\"%d.%m.%Y\")}}|{{value.date.strftime(\"%a\")}}"; types = [ "Restmüll" ]; } { platform = "waste_collection_schedule"; name = "papiermuell_upcoming"; value_template = "{{value.types|join(\", \")}}|{{value.daysTo}}|{{value.date.strftime(\"%d.%m.%Y\")}}|{{value.date.strftime(\"%a\")}}"; types = [ "Papiermüll" ]; } ]; }; extraPackages = python3Packages: with python3Packages; [ adguardhome aioelectricitymaps dwdwfsapi fritzconnection getmac gtts psycopg2 pyatv pyipp pymodbus zlib-ng ]; }; age.secrets."home-assistant-secrets.yaml" = { rekeyFile = ./secrets/home-assistant-secrets.yaml.age; owner = "hass"; }; systemd.services.home-assistant = { serviceConfig.LoadCredential = [ "hass-influxdb-token:${config.age.secrets.hass-influxdb-token.path}" ]; preStart = lib.mkBefore '' if [[ -e ${config.services.home-assistant.configDir}/secrets.yaml ]]; then rm ${config.services.home-assistant.configDir}/secrets.yaml fi # Update influxdb token # We don't use -i because it would require chown with is a @privileged syscall INFLUXDB_TOKEN="$(cat "$CREDENTIALS_DIRECTORY/hass-influxdb-token")" \ ${lib.getExe pkgs.yq-go} '.influxdb_token = strenv(INFLUXDB_TOKEN)' \ ${ config.age.secrets."home-assistant-secrets.yaml".path } > ${config.services.home-assistant.configDir}/secrets.yaml touch -a ${config.services.home-assistant.configDir}/{automations,scenes,scripts,manual}.yaml ''; }; # Connect to fritzbox via https proxy (to ensure valid cert) networking.hosts.${globals.net.home-lan.vlans.services.hosts.ward-web-proxy.ipv4} = [ fritzboxDomain ]; networking.hosts.${globals.wireguard.proxy-home.hosts.ward-adguardhome.ipv4} = [ "adguardhome.internal" ]; nodes.ward-web-proxy = { services.nginx = { upstreams."home-assistant" = { servers."${ globals.wireguard.proxy-home.hosts.${config.node.name}.ipv4 }:${toString config.services.home-assistant.config.http.server_port}" = { }; extraConfig = '' zone home-assistant 64k; keepalive 2; ''; }; virtualHosts.${homeassistantDomain} = { forceSSL = true; useACMEWildcardHost = true; locations."/" = { proxyPass = "http://home-assistant"; proxyWebsockets = true; }; extraConfig = '' allow ${globals.net.home-lan.vlans.home.cidrv4}; allow ${globals.net.home-lan.vlans.home.cidrv6}; allow ${globals.net.home-lan.vlans.devices.cidrv4}; allow ${globals.net.home-lan.vlans.devices.cidrv6}; # Self-traffic (needed for media in Voice PE) allow ${globals.net.home-lan.vlans.services.hosts.sausebiene.ipv4}; allow ${globals.net.home-lan.vlans.services.hosts.sausebiene.ipv6}; # Firezone traffic allow ${globals.net.home-lan.vlans.services.hosts.ward.ipv4}; allow ${globals.net.home-lan.vlans.services.hosts.ward.ipv6}; deny all; ''; }; }; }; }