NixOS Coffee

Yeah but will NixOS make my coffee in the morning?

NixOS 21.11 was released a few days ago, so in the process of spamming nixos links in the usual places someone responded with a question for the ages:

question

So I responded with some pseudocode that should accomplish that:

  services.coffee = {
    enable = true;
    time = "14:00";
  };

To my surprise, nixos doesn't actually have a module for this, though. I have an ubiquiti mfi networked power strip with a fairly simple API so I decide to make one. This is simple enough that it can be lifted straight from the nixos manual's locate service example.

First the boilerplate:

{ config, pkgs, lib, ... }:
let
  cfg = config.services.coffee;
in {

}

Define the interface:

  options.services.coffee = {
    enable = lib.mkEnableOption "brew coffee";
    time = lib.mkOption {
      type = lib.types.str;
      default = "09:00";
      description = ''
        Turn on kettle at 09:00 via api call to power strip
      '';
    };
    sessionID = lib.mkOption {
      type = lib.types.str;
      default = "01234567890123456789012345678901";
      description = ''
        Session ID for the api request
      '';
    };
    ipAddress = lib.mkOption {
      type = lib.types.str;
      default = "10.200.1.5";
      description = ''
        IP address of the mfi device
      '';
    };
    apicall = lib.mkOption {
      type = lib.types.str;
      default =  "curl -X POST -d \"username=ubnt&password=ubnt\" -b \"AIROS_SESSIONID=${cfg.sessionID}\" http://${cfg.ipAddress}/login.cgi && curl -X PUT -d output=1 -b \"AIROS_SESSIONID=${cfg.sessionID}\" http://${cfg.ipAddress}/sensors/1";
    };
  };

Add the systemd service and timer:

  config = {
      systemd.services.brew-coffee = {
        description = "Turn on power strip";
        path = [ pkgs.curl ];
        script = ''
          ${cfg.apicall}
        '';
      };
      systemd.timers.brew-coffee = {
        description = "Update time";
        partOf = [ "brew-coffee.service" ];
        wantedBy = [ "timers.target" ];
        timerConfig.OnCalendar = cfg.time;
      };
  };

All of that wrapped up in a nice christmas package:

{ config, pkgs, lib, ... }:
let
  cfg = config.services.coffee;
in {
  options.services.coffee = {
    enable = lib.mkEnableOption "brew coffee";
    time = lib.mkOption {
      type = lib.types.str;
      default = "09:00";
      description = ''
        Turn on kettle at 09:00 via api call to power strip
      '';
    };
    sessionID = lib.mkOption {
      type = lib.types.str;
      default = "01234567890123456789012345678901";
      description = ''
        Session ID for the api request
      '';
    };
    ipAddress = lib.mkOption {
      type = lib.types.str;
      default = "10.200.1.5";
      description = ''
        IP address of the mfi device
      '';
    };
    apicall = lib.mkOption {
      type = lib.types.str;
      default =  "curl -X POST -d \"username=ubntmin&password=ubnt\" -b \"AIROS_SESSIONID=${cfg.sessionID}\" http://${cfg.ipAddress}/login.cgi && curl -X PUT -d output=1 -b \"AIROS_SESSIONID=${cfg.sessionID}\" http://${cfg.ipAddress}/sensors/1";
    };
  };
  config = {
      systemd.services.brew-coffee = {
        description = "Heats water for coffee";
        path = [ pkgs.curl ];
        script = ''
          ${cfg.apicall}
        '';
      };
      systemd.timers.brew-coffee = {
        description = "Coffee timer";
        partOf = [ "brew-coffee.service" ];
        wantedBy = [ "timers.target" ];
        timerConfig.OnCalendar = cfg.time;
      };
  };
}

Add the module to my config:

  imports = [
    ./coffee.nix
  ];
  services.coffee = {
    enable = true;
    time = "15:00";
  };

Time to rebuild the system and see what needs to be fixed, but

rebuilt

To my surprise it actually worked first try (forgetting to include lib up top notwithstanding). Sweet! A few minutes later I hear a relay click and check the power strip to confirm the good news.

brewing

And the services:

status

That was surprisingly headache free.

A small confession: It actually just started the kettle, but that's close enough for government work.