nixos/esphome: init module

oddlama 8df62ec4 8da0c399

Changed files
+181
nixos
doc
manual
release-notes
modules
services
home-automation
tests
+2
nixos/doc/manual/release-notes/rl-2305.section.md
···
- [atuin](https://github.com/ellie/atuin), a sync server for shell history. Available as [services.atuin](#opt-services.atuin.enable).
+
- [esphome](https://esphome.io), a dashboard to configure ESP8266/ESP32 devices for use with Home Automation systems. Available as [services.esphome](#opt-services.esphome.enable).
+
- [networkd-dispatcher](https://gitlab.com/craftyguy/networkd-dispatcher), a dispatcher service for systemd-networkd connection status changes. Available as [services.networkd-dispatcher](#opt-services.networkd-dispatcher.enable).
- [mmsd](https://gitlab.com/kop316/mmsd), a lower level daemon that transmits and recieves MMSes. Available as [services.mmsd](#opt-services.mmsd.enable).
+1
nixos/modules/module-list.nix
···
./services/hardware/usbrelayd.nix
./services/hardware/vdr.nix
./services/hardware/keyd.nix
+
./services/home-automation/esphome.nix
./services/home-automation/evcc.nix
./services/home-automation/home-assistant.nix
./services/home-automation/zigbee2mqtt.nix
+136
nixos/modules/services/home-automation/esphome.nix
···
+
{ config, lib, pkgs, ... }:
+
+
let
+
inherit (lib)
+
literalExpression
+
maintainers
+
mkEnableOption
+
mkIf
+
mkOption
+
mdDoc
+
types
+
;
+
+
cfg = config.services.esphome;
+
+
stateDir = "/var/lib/esphome";
+
+
esphomeParams =
+
if cfg.enableUnixSocket
+
then "--socket /run/esphome/esphome.sock"
+
else "--address ${cfg.address} --port ${toString cfg.port}";
+
in
+
{
+
meta.maintainers = with maintainers; [ oddlama ];
+
+
options.services.esphome = {
+
enable = mkEnableOption (mdDoc "esphome");
+
+
package = mkOption {
+
type = types.package;
+
default = pkgs.esphome;
+
defaultText = literalExpression "pkgs.esphome";
+
description = mdDoc "The package to use for the esphome command.";
+
};
+
+
enableUnixSocket = mkOption {
+
type = types.bool;
+
default = false;
+
description = lib.mdDoc "Listen on a unix socket `/run/esphome/esphome.sock` instead of the TCP port.";
+
};
+
+
address = mkOption {
+
type = types.str;
+
default = "localhost";
+
description = mdDoc "esphome address";
+
};
+
+
port = mkOption {
+
type = types.port;
+
default = 6052;
+
description = mdDoc "esphome port";
+
};
+
+
openFirewall = mkOption {
+
default = false;
+
type = types.bool;
+
description = mdDoc "Whether to open the firewall for the specified port.";
+
};
+
+
allowedDevices = mkOption {
+
default = ["char-ttyS" "char-ttyUSB"];
+
example = ["/dev/serial/by-id/usb-Silicon_Labs_CP2102_USB_to_UART_Bridge_Controller_0001-if00-port0"];
+
description = lib.mdDoc ''
+
A list of device nodes to which {command}`esphome` has access to.
+
Refer to DeviceAllow in systemd.resource-control(5) for more information.
+
Beware that if a device is referred to by an absolute path instead of a device category,
+
it will only allow devices that already are plugged in when the service is started.
+
'';
+
type = types.listOf types.str;
+
};
+
};
+
+
config = mkIf cfg.enable {
+
networking.firewall.allowedTCPPorts = mkIf (cfg.openFirewall && !cfg.enableUnixSocket) [cfg.port];
+
+
systemd.services.esphome = {
+
description = "ESPHome dashboard";
+
after = ["network.target"];
+
wantedBy = ["multi-user.target"];
+
path = [cfg.package];
+
+
# platformio fails to determine the home directory when using DynamicUser
+
environment.PLATFORMIO_CORE_DIR = "${stateDir}/.platformio";
+
+
serviceConfig = {
+
ExecStart = "${cfg.package}/bin/esphome dashboard ${esphomeParams} ${stateDir}";
+
DynamicUser = true;
+
User = "esphome";
+
Group = "esphome";
+
WorkingDirectory = stateDir;
+
StateDirectory = "esphome";
+
StateDirectoryMode = "0750";
+
Restart = "on-failure";
+
RuntimeDirectory = mkIf cfg.enableUnixSocket "esphome";
+
RuntimeDirectoryMode = "0750";
+
+
# Hardening
+
CapabilityBoundingSet = "";
+
LockPersonality = true;
+
MemoryDenyWriteExecute = true;
+
DevicePolicy = "closed";
+
DeviceAllow = map (d: "${d} rw") cfg.allowedDevices;
+
SupplementaryGroups = ["dialout"];
+
#NoNewPrivileges = true; # Implied by DynamicUser
+
PrivateUsers = true;
+
#PrivateTmp = true; # Implied by DynamicUser
+
ProtectClock = true;
+
ProtectControlGroups = true;
+
ProtectHome = true;
+
ProtectHostname = true;
+
ProtectKernelLogs = true;
+
ProtectKernelModules = true;
+
ProtectKernelTunables = true;
+
ProtectProc = "invisible";
+
ProcSubset = "pid";
+
ProtectSystem = "strict";
+
#RemoveIPC = true; # Implied by DynamicUser
+
RestrictAddressFamilies = [
+
"AF_INET"
+
"AF_INET6"
+
"AF_NETLINK"
+
"AF_UNIX"
+
];
+
RestrictNamespaces = false; # Required by platformio for chroot
+
RestrictRealtime = true;
+
#RestrictSUIDSGID = true; # Implied by DynamicUser
+
SystemCallArchitectures = "native";
+
SystemCallFilter = [
+
"@system-service"
+
"@mount" # Required by platformio for chroot
+
];
+
UMask = "0077";
+
};
+
};
+
};
+
}
+1
nixos/tests/all-tests.nix
···
envoy = handleTest ./envoy.nix {};
ergo = handleTest ./ergo.nix {};
ergochat = handleTest ./ergochat.nix {};
+
esphome = handleTest ./esphome.nix {};
etc = pkgs.callPackage ../modules/system/etc/test.nix { inherit evalMinimalConfig; };
activation = pkgs.callPackage ../modules/system/activation/test.nix { };
etcd = handleTestOn ["x86_64-linux"] ./etcd.nix {};
+41
nixos/tests/esphome.nix
···
+
import ./make-test-python.nix ({ pkgs, lib, ... }:
+
+
let
+
testPort = 6052;
+
unixSocket = "/run/esphome/esphome.sock";
+
in
+
with lib;
+
{
+
name = "esphome";
+
meta.maintainers = with pkgs.lib.maintainers; [ oddlama ];
+
+
nodes = {
+
esphomeTcp = { ... }:
+
{
+
services.esphome = {
+
enable = true;
+
port = testPort;
+
address = "0.0.0.0";
+
openFirewall = true;
+
};
+
};
+
+
esphomeUnix = { ... }:
+
{
+
services.esphome = {
+
enable = true;
+
enableUnixSocket = true;
+
};
+
};
+
};
+
+
testScript = ''
+
esphomeTcp.wait_for_unit("esphome.service")
+
esphomeTcp.wait_for_open_port(${toString testPort})
+
esphomeTcp.succeed("curl --fail http://localhost:${toString testPort}/")
+
+
esphomeUnix.wait_for_unit("esphome.service")
+
esphomeUnix.wait_for_file("${unixSocket}")
+
esphomeUnix.succeed("curl --fail --unix-socket ${unixSocket} http://localhost/")
+
'';
+
})