feat: configure homeassistant and esphome on new machine

This commit is contained in:
oddlama 2025-01-26 01:43:01 +01:00
parent 0ff0828ca9
commit 3d37e2959f
No known key found for this signature in database
GPG key ID: 14EFE510775FE39A
52 changed files with 403 additions and 672 deletions

View file

@ -19,6 +19,10 @@
./fs.nix
./net.nix
./esphome.nix
./home-assistant.nix
./mosquitto.nix
];
topology.self.hardware.info = "Intel N100, 16GB RAM";

View file

@ -0,0 +1,59 @@
{
config,
globals,
...
}:
let
esphomeDomain = "esphome.${globals.domains.personal}";
in
{
wireguard.proxy-home.firewallRuleForNode.ward-web-proxy.allowedTCPPorts = [
config.services.esphome.port
];
environment.persistence."/persist".directories = [
{
directory = "/var/lib/private/esphome";
mode = "0700";
}
];
globals.services.esphome.domain = esphomeDomain;
# globals.monitoring.http.esphome = {
# url = "https://${esphomeDomain}";
# expectedBodyRegex = "esphome";
# network = "internet";
# };
topology.self.services.esphome.info = "https://${esphomeDomain}";
services.esphome = {
enable = true;
address = "0.0.0.0";
port = 3001;
};
nodes.ward-web-proxy = {
services.nginx = {
upstreams."esphome" = {
servers."${config.wireguard.proxy-home.ipv4}:${toString config.services.esphome.port}" = { };
extraConfig = ''
zone esphome 64k;
keepalive 2;
'';
};
virtualHosts.${esphomeDomain} = {
forceSSL = true;
useACMEWildcardHost = true;
locations."/" = {
proxyPass = "http://esphome";
proxyWebsockets = true;
};
extraConfig = ''
allow ${globals.net.home-lan.vlans.home.cidrv4};
allow ${globals.net.home-lan.vlans.home.cidrv6};
deny all;
'';
};
};
};
}

View file

@ -0,0 +1,192 @@
{
config,
globals,
lib,
nodes,
pkgs,
...
}:
let
homeassistantDomain = "home.${globals.domains.personal}";
fritzboxDomain = "fritzbox.${globals.domains.me}";
in
{
wireguard.proxy-home.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";
# };
topology.self.services.home-assistant.info = "https://${homeassistantDomain}";
services.home-assistant = {
enable = true;
extraComponents = [
"radio_browser"
"met"
"esphome"
"fritzbox"
"soundtouch"
"spotify"
"matter"
#"zha"
"mqtt"
"ollama"
];
customLovelaceModules =
let
mods = pkgs.home-assistant-custom-lovelace-modules;
in
[
mods.bubble-card
mods.weather-card
mods.mini-graph-card
mods.card-mod
mods.mushroom
mods.multiple-entity-row
mods.button-card
mods.weather-chart-card
mods.hourly-weather
];
config = {
default_config = { };
http = {
server_host = [ "0.0.0.0" ];
server_port = 8123;
use_x_forwarded_for = true;
trusted_proxies = [ nodes.ward-web-proxy.config.wireguard.proxy-home.ipv4 ];
};
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://";
packages.manual = "!include manual.yaml";
};
lovelace.mode = "yaml";
frontend = {
themes = "!include_dir_merge_named themes";
};
"automation ui" = "!include automations.yaml";
# influxdb = {
# api_version = 2;
# host = globals.services.influxdb.domain;
# port = "443";
# max_retries = 10;
# ssl = true;
# verify_ssl = true;
# token = "!secret influxdb_token";
# organization = "home";
# bucket = "home_assistant";
# };
};
extraPackages =
python3Packages: with python3Packages; [
psycopg2
gtts
fritzconnection
adguardhome
zlib-ng
pymodbus
];
};
age.secrets."home-assistant-secrets.yaml" = {
rekeyFile = ./secrets/home-assistant-secrets.yaml.age;
owner = "hass";
};
systemd.services.home-assistant = {
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 ${config.age.secrets.hass-influxdb-token.path})" \
${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
'';
};
age.secrets.hass-influxdb-token = {
generator.script = "alnum";
mode = "440";
group = "hass";
};
# nodes.sire-influxdb = {
# # Mirror the original secret on the influx host
# age.secrets."hass-influxdb-token-${config.node.name}" = {
# inherit (config.age.secrets.hass-influxdb-token) rekeyFile;
# mode = "440";
# group = "influxdb2";
# };
#
# services.influxdb2.provision.organizations.home.auths."home-assistant (${config.node.name})" = {
# readBuckets = [ "home_assistant" ];
# writeBuckets = [ "home_assistant" ];
# tokenFile = nodes.sire-influxdb.config.age.secrets."hass-influxdb-token-${config.node.name}".path;
# };
# };
# Connect to fritzbox via https proxy (to ensure valid cert)
networking.hosts.${globals.net.home-lan.vlans.services.hosts.ward-web-proxy.ipv4} = [
fritzboxDomain
];
nodes.ward-web-proxy = {
services.nginx = {
upstreams."home-assistant" = {
servers."${config.wireguard.proxy-home.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};
deny all;
'';
};
};
};
}

View file

@ -0,0 +1,42 @@
{ config, ... }:
{
age.secrets.mosquitto-pw-home-assistant = {
mode = "440";
owner = "hass";
group = "mosquitto";
generator.script = "alnum";
};
services.mosquitto = {
enable = true;
persistence = true;
listeners = [
{
acl = [ "pattern readwrite #" ];
users = {
# zigbee2mqtt = {
# passwordFile = config.age.secrets.mosquitto-pw-zigbee2mqtt.path;
# acl = [ "readwrite #" ];
# };
home_assistant = {
passwordFile = config.age.secrets.mosquitto-pw-home-assistant.path;
acl = [ "readwrite #" ];
};
};
settings.allow_anonymous = false;
}
];
};
networking.nftables.firewall.rules = {
# Allow devices and iot VLANs to access the MQTT server
access-mqtt = {
from = [
"vlan-devices"
"vlan-iot"
];
to = [ "local" ];
allowedTCPPorts = [ 1883 ];
};
};
}

View file

@ -1,42 +1,110 @@
{
config,
globals,
lib,
...
}:
let
localVlans = lib.genAttrs [ "services" "home" "devices" "iot" ] (
x: globals.net.home-lan.vlans.${x}
);
in
{
networking.hostId = config.repo.secrets.local.networking.hostId;
# FIXME: aaaaaaaaa
# globals.monitoring.ping.sausebiene = {
# hostv4 = lib.net.cidr.ip globals.net.home-lan.vlans.services.hosts.sausebiene.cidrv4;
# hostv6 = lib.net.cidr.ip globals.net.home-lan.vlans.services.hosts.sausebiene.cidrv6;
# network = "home-lan.vlans.services";
# };
globals.monitoring.ping.sausebiene = {
hostv4 = lib.net.cidr.ip globals.net.home-lan.vlans.services.hosts.sausebiene.cidrv4;
hostv6 = lib.net.cidr.ip globals.net.home-lan.vlans.services.hosts.sausebiene.cidrv6;
network = "home-lan.vlans.services";
};
boot.initrd.availableKernelModules = [ "8021q" ];
boot.initrd.systemd.network = {
enable = true;
netdevs."30-vlan-services" = {
netdevConfig = {
Kind = "vlan";
Name = "vlan-services";
};
vlanConfig.Id = globals.net.home-lan.vlans.services.id;
};
networks = {
inherit (config.systemd.network.networks) "10-lan";
"10-lan" = {
matchConfig.Name = "lan";
networkConfig.LinkLocalAddressing = "no";
linkConfig.RequiredForOnline = "carrier";
vlan = [ "vlan-services" ];
};
"30-vlan-services" = {
address = [
globals.net.home-lan.vlans.services.hosts.sausebiene.cidrv4
globals.net.home-lan.vlans.services.hosts.sausebiene.cidrv6
];
gateway = [ globals.net.home-lan.vlans.services.hosts.ward.ipv4 ];
matchConfig.Name = "vlan-services";
networkConfig = {
IPv6PrivacyExtensions = "yes";
MulticastDNS = true;
};
linkConfig.RequiredForOnline = "routable";
};
};
};
systemd.network.networks = {
"10-lan" = {
address = [ "192.168.1.17/24" ];
gateway = [ "192.168.1.1" ];
matchConfig.MACAddress = config.repo.secrets.local.networking.interfaces.lan.mac;
networkConfig = {
IPv6PrivacyExtensions = "yes";
MulticastDNS = true;
systemd.network.netdevs = lib.flip lib.concatMapAttrs localVlans (
vlanName: vlanCfg: {
# Add an interface for each VLAN
"30-vlan-${vlanName}" = {
netdevConfig = {
Kind = "vlan";
Name = "vlan-${vlanName}";
};
vlanConfig.Id = vlanCfg.id;
};
linkConfig.RequiredForOnline = "routable";
};
};
}
);
systemd.network.networks =
{
"10-lan" = {
matchConfig.Name = "lan";
# This interface should only be used from attached vlans.
# So don't acquire a link local address and only wait for
# this interface to gain a carrier.
networkConfig.LinkLocalAddressing = "no";
linkConfig.RequiredForOnline = "carrier";
vlan = map (name: "vlan-${name}") (builtins.attrNames localVlans);
};
}
// lib.flip lib.concatMapAttrs localVlans (
vlanName: vlanCfg: {
"30-vlan-${vlanName}" = {
address = [
vlanCfg.hosts.sausebiene.cidrv4
vlanCfg.hosts.sausebiene.cidrv6
];
gateway = [ vlanCfg.hosts.ward.ipv4 ];
matchConfig.Name = "vlan-${vlanName}";
networkConfig = {
IPv6PrivacyExtensions = "yes";
MulticastDNS = true;
};
linkConfig.RequiredForOnline = "routable";
};
}
);
networking.nftables.firewall = {
zones.untrusted.interfaces = [ "lan" ];
zones =
{
untrusted.interfaces = [ "vlan-services" ];
}
// lib.flip lib.concatMapAttrs localVlans (
vlanName: _: {
"vlan-${vlanName}".interfaces = [ "vlan-${vlanName}" ];
}
);
};
# Allow accessing influx
wireguard.proxy-sentinel.client.via = "sentinel";
wireguard.proxy-home.client.via = "ward";
}

View file

@ -0,0 +1,10 @@
age-encryption.org/v1
-> X25519 /fHu4GoqBkVzZqZJ38xy8XbcWQ6SF3X6rvYjFv8gums
4FPbuUEYdrFpv72oo8+VL8rxdQzDFMgy7lfYp/e6PWc
-> piv-p256 xqSe8Q A7xG4f2f/SRpM1RIQSVL9q8g/AzVcIrDWq7nGDJQimQo
rL9Wgz4z18F5Qn+5Z20N7356YVLLrJvtvtGgx0jJwm4
-> u[-grease ad
4NbLgEGN91yifuQh9zzwJegrU3ZvxOqtHsCn3XAXpQpv0x9f0HXMGJ2HJnB3dNXL
bxLtOZDlNinTOnR0p6ygxhg
--- uCx7X+ivq3iUCwYZjIcNZfHgfkzeuTGnG7lsVyKLqTk
ßSª#:.éá}"5Â�˜:¦Ÿ¢—ÂM1Cʇug-é6UºFfÎ:kj�qœ’0ô‡”`~’IÐÿTL{P5ðZ3tw¥]¯º*YŠOçYS9¢üË"#ø9Ô|WE"—íz4G“tÖ!k\YÒ—O(�{úð”~¡ßGˆWÕ_îÎàhŸ$@

View file

@ -1 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIC+ziFZSELVG9MmbkMDE9xwKHlm4lnr2uHtVNXk+rTu
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINBDwxSNMyY1EF+xQP+hQ/d/mfK6PapwUWiuDtvealr0