From 2a135612abc66841e99671e373e85194e0afe333 Mon Sep 17 00:00:00 2001
From: Felix Tenley <dev@felschr.com>
Date: Wed, 7 Oct 2020 14:37:57 +0200
Subject: [PATCH] fix(rpi4): set up deconz

---
 flake.nix                   |   7 +-
 pkgs/deconz/default.nix     |  42 ++++++++++++
 services/deconz.nix         | 123 ++++++++++++++++++++++++++++++++++++
 services/home-assistant.nix |  34 ++++++++++
 4 files changed, 205 insertions(+), 1 deletion(-)
 create mode 100644 pkgs/deconz/default.nix
 create mode 100644 services/deconz.nix

diff --git a/flake.nix b/flake.nix
index 7034e08..d757a56 100644
--- a/flake.nix
+++ b/flake.nix
@@ -29,7 +29,12 @@
 
           nix.registry.nixpkgs.flake = nixpkgs;
 
-          nixpkgs.overlays = [ nur.overlay ];
+          nixpkgs.overlays = [
+            nur.overlay
+	    (self: super: {
+              deconz = pkgs.qt5.callPackage ./pkgs/deconz { };
+            })
+          ];
 
           imports =
             [ hardwareConfig home-manager.nixosModules.home-manager config ];
diff --git a/pkgs/deconz/default.nix b/pkgs/deconz/default.nix
new file mode 100644
index 0000000..252c300
--- /dev/null
+++ b/pkgs/deconz/default.nix
@@ -0,0 +1,42 @@
+{ stdenv, fetchurl, mkDerivation, dpkg, autoPatchelfHook
+, qtserialport, qtwebsockets
+, libredirect, makeWrapper, gzip, gnutar
+}:
+
+mkDerivation rec {
+  name = "deconz-${version}";
+  version = "2.05.82";
+
+  src = fetchurl {
+    url = "https://deconz.dresden-elektronik.de/raspbian/alpha/deconz_${version}-debian-stretch-beta_arm64.deb";
+    sha256 = "cCH7XhRXCHKm5AVsM19TyHwAjhbTv4qyDx2GamuDWQw=";
+  };
+
+  nativeBuildInputs = [ dpkg autoPatchelfHook makeWrapper ];
+
+  buildInputs = [ qtserialport qtwebsockets ];
+
+  unpackPhase = "dpkg-deb -x $src .";
+
+  installPhase = ''
+    mkdir -p "$out"
+    cp -r usr/* .
+    cp -r share/deCONZ/plugins/* lib/
+    cp -r . $out
+
+    wrapProgram "$out/bin/deCONZ" \
+        --set LD_PRELOAD "${libredirect}/lib/libredirect.so" \
+        --set NIX_REDIRECTS "/usr/share=$out/share:/usr/bin=$out/bin" \
+        --prefix PATH : "${stdenv.lib.makeBinPath [ gzip gnutar ]}"
+  '';
+
+  meta = with stdenv.lib; {
+    description = "Manage ZigBee network with ConBee, ConBee II or RaspBee hardware";
+    # 2019-08-19: The homepage links to old software that doesn't even work --
+    # it fails to detect ConBee2.
+    homepage = "https://www.dresden-elektronik.de/funktechnik/products/software/pc-software/deconz/?L=1";
+    license = licenses.unfree;
+    platforms = with platforms; linux;
+    maintainers = with maintainers; [ felschr ];
+  };
+}
diff --git a/services/deconz.nix b/services/deconz.nix
new file mode 100644
index 0000000..8047ac9
--- /dev/null
+++ b/services/deconz.nix
@@ -0,0 +1,123 @@
+# FIXME: These two auth issues:
+# https://github.com/dresden-elektronik/deconz-rest-plugin/issues/1788 ("Auth problems on non-80 http port")
+# https://github.com/dresden-elektronik/deconz-rest-plugin/issues/1792 ("Trying to change password: "Service not available. Try again later.")
+
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.local.services.deconz;
+  name = "deconz";
+  stateDir = "/var/lib/${name}";
+in
+{
+  options.local.services.deconz = {
+
+    enable = mkEnableOption "deCONZ, a ZigBee gateway";
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.deconz;
+      defaultText = "pkgs.deconz";
+      description = "Which deCONZ package to use.";
+    };
+
+    device = mkOption {
+      type = types.str;
+      default = "";
+      description = ''
+        Force deCONZ to use a specific USB device (e.g. /dev/ttyACM0). By
+        default it does a search.
+      '';
+    };
+
+    httpPort = mkOption {
+      type = types.port;
+      default = 80;
+      description = "TCP port for the web server.";
+    };
+
+    wsPort = mkOption {
+      type = types.port;
+      default = 443;
+      description = "TCP port for the WebSocket.";
+    };
+
+    openFirewall = mkEnableOption "open up the service ports in the firewall";
+
+    allowRebootSystem = mkEnableOption "allow rebooting the system";
+
+    allowRestartService = mkEnableOption "allow killing/restarting processes";
+
+    allowSetSystemTime = mkEnableOption "allow setting the system time";
+
+    extraOpts = mkOption {
+      type = types.listOf types.str;
+      default = [
+        "--auto-connect=1"
+        "--dbg-info=1"
+      ];
+      description = ''
+        Extra command line options for deCONZ.
+        These options seem undocumented, but some examples can be found here:
+        https://github.com/marthoc/docker-deconz/blob/master/amd64/root/start.sh
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewall [
+      cfg.httpPort
+      cfg.wsPort
+    ];
+
+    systemd.services.deconz = {
+      description = "deCONZ ZigBee gateway";
+      wantedBy = [ "multi-user.target" ];
+
+      preStart = ''
+        # The service puts a nix store path reference in here, and that path can
+        # be garbage collected. Ensure the file gets "refreshed" on every start.
+        rm -f ${stateDir}/.local/share/dresden-elektronik/deCONZ/zcldb.txt
+      '';
+      serviceConfig = {
+        ExecStart =
+          "${cfg.package}/bin/deCONZ"
+          + " -platform minimal"
+          + " --http-port=${toString cfg.httpPort}"
+          + " --ws-port=${toString cfg.wsPort}"
+          + (if cfg.device != "" then " --dev=${cfg.device}" else "")
+          + " " + (lib.concatStringsSep " " cfg.extraOpts);
+        Restart = "on-failure";
+        AmbientCapabilities =
+          let
+            # ref. upstream deconz.service
+            caps = lib.optionals (cfg.httpPort < 1024 || cfg.wsPort < 1024) [ "CAP_NET_BIND_SERVICE" ]
+                ++ lib.optionals (cfg.allowRebootSystem) [ "CAP_SYS_BOOT" ]
+                ++ lib.optionals (cfg.allowRestartService) [ "CAP_KILL" ]
+                ++ lib.optionals (cfg.allowSetSystemTime) [ "CAP_SYS_TIME" ];
+          in
+            lib.concatStringsSep " " caps;
+        UMask = "0027";
+        User = name;
+        StateDirectory = name;
+        WorkingDirectory = stateDir;
+
+        ProtectSystem = "strict";
+        ProtectHome = true;
+        ReadWritePaths = "/tmp";
+      };
+    };
+
+    users.users.deconz = {
+      group = name;
+      isSystemUser = true;
+      home = stateDir;
+      extraGroups = [ "dialout" ];  # for access to /dev/ttyACM0 (ConBee)
+    };
+
+    users.groups.deconz = {};
+  };
+}
diff --git a/services/home-assistant.nix b/services/home-assistant.nix
index 04359f9..3a68bb9 100644
--- a/services/home-assistant.nix
+++ b/services/home-assistant.nix
@@ -1,9 +1,38 @@
 { config, pkgs, ... }:
 
 with pkgs;
+let
+  pydeconz = python3.pkgs.buildPythonPackage rec {
+    pname = "pydeconz";
+    version = "73";
+
+    src = python3.pkgs.fetchPypi {
+      inherit pname version;
+      sha256 = "Lm7J0p2dp2gyesDpgN0WGpxPewC1z/IUy0CDEqofQGA=";
+    };
+
+    propagateBuildInputs = with python3Packages; [ setuptools ];
+    buildInputs = with python3Packages; [ aiohttp ];
+    doCheck = false;
+  };
+in
 {
+  imports = [ ./deconz.nix ];
+
+  environment.systemPackages = with pkgs; [ deconz ];
+
+  local.services.deconz = {
+    enable = true;
+    httpPort = 8080;
+    wsPort = 1443;
+    openFirewall = true;
+  };
+
   services.home-assistant = {
     enable = true;
+    package = home-assistant.override {
+      extraPackages = ps: with ps; [ pydeconz ];
+    };
     openFirewall = true;
     config = {
       homeassistant = {
@@ -28,6 +57,11 @@ with pkgs;
         mqtt_topic = "owntracks/#";
 	secret = "!secret owntracks_secret";
       };
+      deconz = {
+        host = "localhost";
+        port = 8080;
+	api_key = "!secret deconz_apikey";
+      };
     };
     # configWritable = true; # doesn't work atm
   };